[opentitanlib] Add OTP hex file generation.

Signed-off-by: Jon Flatley <jflat@google.com>
diff --git a/sw/host/opentitanlib/src/lib.rs b/sw/host/opentitanlib/src/lib.rs
index 38e9c55..71ddeab 100644
--- a/sw/host/opentitanlib/src/lib.rs
+++ b/sw/host/opentitanlib/src/lib.rs
@@ -5,6 +5,7 @@
 pub mod app;
 pub mod bootstrap;
 pub mod io;
+pub mod otp;
 pub mod spiflash;
 pub mod transport;
 pub mod util;
diff --git a/sw/host/opentitanlib/src/otp/lc_state.rs b/sw/host/opentitanlib/src/otp/lc_state.rs
new file mode 100644
index 0000000..813d8da
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/lc_state.rs
@@ -0,0 +1,110 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{bail, Result};
+use serde::Deserialize;
+use std::fs;
+use std::path::Path;
+
+/// SECDED matrix used for ECC in OTP.
+#[derive(Deserialize, Debug)]
+pub struct LcSecded {
+    /// The number of bits of data covered by ECC.
+    data_width: usize,
+    /// The number of ECC bits.
+    ecc_width: usize,
+    /// ECC matrix used for computing ECC bits.
+    ecc_matrix: Vec<Vec<u8>>,
+}
+
+/// The internal representation of lc_ctrl_state, used in OTP operations.
+#[derive(Deserialize, Debug)]
+pub struct LcState {
+    secded: LcSecded,
+}
+
+impl LcSecded {
+    pub fn new(in_file: &Path) -> Result<LcSecded> {
+        let json_text = fs::read_to_string(in_file)?;
+        let res: LcState = deser_hjson::from_str(&json_text)?;
+        if res.secded.ecc_matrix.len() != res.secded.ecc_width {
+            bail!("Bad ecc matrix length {}", res.secded.ecc_matrix.len());
+        }
+        Ok(res.secded)
+    }
+
+    fn bit_index(data: &[u8], index: usize) -> bool {
+        let byte = index / 8;
+        let bit = index % 8;
+        data[byte] & (1 << bit) != 0
+    }
+
+    pub fn ecc_encode(&self, mut data: Vec<u8>) -> Result<Vec<u8>> {
+        if data.len() * 8 != self.data_width {
+            bail!("Bad data length for ecc {}", data.len() * 8);
+        }
+        let data_len = data.len();
+        data.resize(data_len + self.ecc_byte_len(), 0);
+        for (i, matrix) in self.ecc_matrix.iter().enumerate() {
+            let mut bit = false;
+            for j in matrix {
+                bit ^= Self::bit_index(&data, *j as usize);
+            }
+            if bit {
+                let byte = i / 8 + data_len;
+                let bit = i % 8;
+                data[byte] |= 1 << bit;
+            }
+        }
+
+        Ok(data)
+    }
+
+    pub fn ecc_byte_len(&self) -> usize {
+        if self.ecc_width == 0 {
+            0
+        } else {
+            (self.ecc_width - 1) / 8 + 1
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use anyhow::Result;
+    use deser_hjson::from_str;
+    use std::fs::read_to_string;
+
+    #[test]
+    fn test_lc_state_deserialize() -> Result<()> {
+        let _: LcState = from_str(&read_to_string("tests/lc_ctrl_state.hjson")?)?;
+        Ok(())
+    }
+
+    #[test]
+    fn test_ecc_encode() {
+        let secded = LcSecded {
+            data_width: 16,
+            ecc_width: 6,
+            ecc_matrix: vec![
+                vec![0, 1, 3, 4, 6, 8, 10, 11, 13, 15], // ECC bit 0
+                vec![0, 2, 3, 5, 6, 9, 10, 12, 13],     // ECC bit 1
+                vec![1, 2, 3, 7, 8, 9, 10, 14, 15],     // ECC bit 2
+                vec![4, 5, 6, 7, 8, 9, 10],             // ECC bit 3
+                vec![11, 12, 13, 14, 15],               // ECC bit 4
+                vec![
+                    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+                ], // Parity bit
+            ],
+        };
+
+        let zero: Vec<u8> = vec![0, 0];
+        let a5a5: Vec<u8> = vec![0xa5, 0xa5];
+        let fcc5: Vec<u8> = vec![0xfc, 0xc5];
+        assert_eq!(vec![0u8, 0, 0], secded.ecc_encode(zero).unwrap());
+        assert_eq!(vec![0xa5u8, 0xa5, 0x27], secded.ecc_encode(a5a5).unwrap());
+        assert_eq!(vec![0x0fcu8, 0xc5, 0x06], secded.ecc_encode(fcc5).unwrap())
+    }
+}
diff --git a/sw/host/opentitanlib/src/otp/mod.rs b/sw/host/opentitanlib/src/otp/mod.rs
new file mode 100644
index 0000000..8465483
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/mod.rs
@@ -0,0 +1,37 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+pub mod lc_state;
+pub mod otp;
+pub mod otp_img;
+pub mod otp_mmap;
+pub mod vmem_serialize;
+
+mod num_de;
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use anyhow::Result;
+    use std::path::Path;
+
+    #[test]
+    fn test_vmem_serialize() -> Result<()> {
+        let mut otp_mmap = otp_mmap::OtpMap::new(Path::new("tests/otp_ctrl_mmap.hjson"))?;
+        let mut otp_img = otp_img::OtpImg::new(Path::new("tests/otp_ctrl_img_dev.hjson"))?;
+        let lc_state = lc_state::LcSecded::new(Path::new("tests/lc_ctrl_state.hjson"))?;
+        let vmem = otp_mmap.make_vmem(&mut otp_img)?;
+        let keys = otp_mmap.generate_keys(&otp_img);
+        let result = vmem.generate(keys, &lc_state)?;
+        let expected = std::fs::read_to_string(Path::new("tests/output.vmem"))?;
+        let expected = expected
+            .split("\n")
+            .filter(|s| !s.is_empty())
+            .collect::<Vec<&str>>();
+
+        assert_eq!(result, expected);
+
+        Ok(())
+    }
+}
diff --git a/sw/host/opentitanlib/src/otp/num_de.rs b/sw/host/opentitanlib/src/otp/num_de.rs
new file mode 100644
index 0000000..d621e9e
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/num_de.rs
@@ -0,0 +1,228 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+/// Deserialization utilities for certain values in OTP HJSON files.
+///
+/// The OTP HJSON files have some strange values:
+///
+/// Integers, sometimes wrapped in strings, with inconsistent formatting and meta values, such as:
+///   - value: "0x739"
+///   - key_size: "16"
+///   - seed: "10556718629619452145"
+///   - seed: 01931961561863975174  // This is a decimal integer, not octal.
+///   - value: "<random>"
+///
+/// Additionally, some values have sizes defined within the config files themselves, such as the
+/// keys. This module exists to handle these peculiar cases.
+use anyhow::Result;
+use rand::RngCore;
+use serde::de::{self, Deserializer, Unexpected};
+use serde::ser::Serializer;
+use serde::{Deserialize, Serialize};
+use std::any::type_name;
+use std::fmt;
+use std::marker::PhantomData;
+use std::ops::Deref;
+
+use crate::util::parse_int::{ParseInt, ParseIntError};
+
+pub fn _serialize<S, T>(_r: T, _ser: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+    T: Serialize + Copy,
+{
+    unimplemented!();
+}
+
+/// Deserialize numeric types from HJSON config files.
+pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+    D: Deserializer<'de>,
+    T: ParseInt,
+{
+    struct Visitor<U>(PhantomData<U>);
+
+    impl<'a, U: ParseInt> de::Visitor<'a> for Visitor<U> {
+        type Value = U;
+
+        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+            formatter.write_fmt(format_args!("a string that parses to {}", type_name::<U>()))
+        }
+
+        fn visit_string<E>(self, mut name: String) -> Result<Self::Value, E>
+        where
+            E: de::Error,
+        {
+            if name.starts_with("false") {
+                name = "0".to_owned()
+            } else if name.starts_with("true") {
+                name = "1".to_owned()
+            }
+
+            let trimmed = if name.starts_with("0x") {
+                &name
+            } else {
+                let trimmed = name[0..name.len() - 1].trim_start_matches('0');
+                &name[name.len() - trimmed.len() - 1..]
+            };
+
+            match U::from_str(trimmed) {
+                Ok(value) => Ok(value),
+                Err(_) => Err(de::Error::invalid_value(Unexpected::Str(trimmed), &self)),
+            }
+        }
+
+        fn visit_str<E>(self, name: &str) -> Result<Self::Value, E>
+        where
+            E: de::Error,
+        {
+            self.visit_string(name.to_owned())
+        }
+
+        fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
+        where
+            E: de::Error,
+        {
+            if v {
+                self.visit_str("1")
+            } else {
+                self.visit_str("0")
+            }
+        }
+    }
+
+    deserializer.deserialize_string(Visitor {
+        0: PhantomData::<T>,
+    })
+}
+
+/// Placeholder type for values that cannot be resolved during deserialization.
+#[derive(Debug, PartialEq, Clone)]
+enum DeferredInit {
+    Initialized(Vec<u8>),
+    Random,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct DeferredValue(#[serde(with = "self")] DeferredInit);
+
+impl DeferredValue {
+    pub fn resolve(&self, size: usize, rng: &mut dyn RngCore) -> Vec<u8> {
+        match self.0.clone() {
+            DeferredInit::Initialized(mut vec) => {
+                vec.resize(size, 0);
+                vec
+            }
+            DeferredInit::Random => {
+                let mut vec = vec![0u8; size];
+                rng.fill_bytes(&mut vec);
+                vec
+            }
+        }
+    }
+
+    pub fn is_initialized(&self) -> bool {
+        matches!(self.0, DeferredInit::Initialized(_))
+    }
+}
+
+impl ParseInt for DeferredInit {
+    type FromStrRadixErr = ParseIntError;
+
+    fn from_str_radix(src: &str, radix: u32) -> Result<Self, ParseIntError> {
+        Ok(DeferredInit::Initialized(Vec::<u8>::from_str_radix(
+            src, radix,
+        )?))
+    }
+
+    fn from_str(src: &str) -> Result<Self, ParseIntError> {
+        if src == "<random>" {
+            Ok(DeferredInit::Random)
+        } else {
+            Ok(DeferredInit::Initialized(Vec::<u8>::from_str(src)?))
+        }
+    }
+}
+
+impl Deref for DeferredValue {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        match &self.0 {
+            DeferredInit::Initialized(val) => val,
+            _ => panic!("Value has not been initialized"),
+        }
+    }
+}
+
+/// Wrapper type to force deserialization assuming octal encoding.
+#[derive(Deserialize, Debug)]
+pub struct OctEncoded<T: ParseInt>(#[serde(with = "self")] T);
+
+/// Wrapper type to force deserialization assuming decimal encoding.
+#[derive(Deserialize, Debug)]
+pub struct DecEncoded<T: ParseInt>(#[serde(with = "self")] T);
+
+/// Wrapper type to force deserialization assuming hexadecimal encoding.
+#[derive(Deserialize, Debug)]
+pub struct HexEncoded<T: ParseInt>(#[serde(with = "self")] T);
+
+macro_rules! impl_parse_int_enc {
+    ($ty:ident, $radix:expr) => {
+        impl<T: ParseInt> std::ops::Deref for $ty<T> {
+            type Target = T;
+
+            fn deref(&self) -> &Self::Target {
+                &self.0
+            }
+        }
+
+        impl<T: ParseInt> ParseInt for $ty<T> {
+            type FromStrRadixErr = T::FromStrRadixErr;
+
+            fn from_str_radix(src: &str, radix: u32) -> Result<Self, T::FromStrRadixErr> {
+                Ok(Self(T::from_str_radix(src, radix)?))
+            }
+
+            fn from_str(src: &str) -> Result<Self, ParseIntError> {
+                Self::from_str_radix(src, $radix).map_err(|e| e.into())
+            }
+        }
+    };
+}
+
+impl_parse_int_enc!(OctEncoded, 8);
+impl_parse_int_enc!(DecEncoded, 10);
+impl_parse_int_enc!(HexEncoded, 16);
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use serde::Deserialize;
+
+    #[test]
+    fn de_u8() -> Result<()> {
+        #[derive(Debug, Deserialize)]
+        struct TestData {
+            #[serde(with = "super")]
+            oct: OctEncoded<u8>,
+            #[serde(with = "super")]
+            dec: DecEncoded<u8>,
+            #[serde(with = "super")]
+            hex: HexEncoded<u8>,
+        }
+
+        let data: TestData = deser_hjson::from_str(stringify!(
+        {
+            oct: "77",
+            dec: "77",
+            hex: "77"
+        }))?;
+
+        assert_eq!(*data.oct, 63);
+        assert_eq!(*data.dec, 77);
+        assert_eq!(*data.hex, 119);
+        Ok(())
+    }
+}
diff --git a/sw/host/opentitanlib/src/otp/otp.rs b/sw/host/opentitanlib/src/otp/otp.rs
new file mode 100644
index 0000000..c11fea0
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/otp.rs
@@ -0,0 +1,44 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::util::parse_int::{ParseInt, ParseIntError};
+
+use anyhow::Result;
+
+impl ParseInt for Vec<u8> {
+    type FromStrRadixErr = ParseIntError;
+
+    fn from_str_radix(src: &str, radix: u32) -> Result<Self, ParseIntError> {
+        let mut bytes = vec![];
+        for digit_bytes in src.as_bytes().rchunks(2) {
+            let digits = std::str::from_utf8(digit_bytes).unwrap();
+            bytes.push(u8::from_str_radix(digits, radix)?);
+        }
+        Ok(bytes)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use std::convert::TryInto;
+    #[test]
+    fn byte_field_test() {
+        assert_eq!(Vec::from_str("0x1"), Ok(vec![0x1]));
+        assert_eq!(
+            Vec::from_str("0x4b4b4b4b4b4ba5a5"),
+            Ok(vec![0xa5, 0xa5, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b])
+        );
+        assert_eq!(
+            u64::from_ne_bytes(
+                Vec::from_str("0x4b4b4b4b4b4ba5a5")
+                    .unwrap()
+                    .try_into()
+                    .unwrap()
+            ),
+            u64::from_str("0x4b4b4b4b4b4ba5a5").unwrap()
+        );
+        assert!(Vec::from_str("-1").is_err());
+    }
+}
diff --git a/sw/host/opentitanlib/src/otp/otp_img.rs b/sw/host/opentitanlib/src/otp/otp_img.rs
new file mode 100644
index 0000000..fe05775
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/otp_img.rs
@@ -0,0 +1,60 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use serde::Deserialize;
+
+use std::path::Path;
+
+use crate::otp::num_de::{DecEncoded, DeferredValue};
+use anyhow::Result;
+use rand::rngs::StdRng;
+use rand::SeedableRng;
+
+const OTP_IMG_SEED_DIVERSIFIER: u64 = 1941661965323525198146u128 as u64;
+
+#[derive(Deserialize, Debug)]
+pub struct OtpImgItem {
+    pub name: String,
+    pub value: DeferredValue,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OtpImgPartition {
+    pub name: String,
+    pub items: Option<Vec<OtpImgItem>>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OtpImg {
+    pub seed: DecEncoded<u64>,
+    pub partitions: Vec<OtpImgPartition>,
+}
+
+impl OtpImgPartition {
+    pub fn get_item(&mut self, name: &str) -> Option<&mut OtpImgItem> {
+        self.items
+            .as_mut()
+            .and_then(|items| items.iter_mut().find(|i| i.name == name))
+    }
+}
+
+impl OtpImg {
+    pub fn new(in_file: &Path) -> Result<OtpImg> {
+        let json_text = std::fs::read_to_string(in_file)?;
+        let res: OtpImg = deser_hjson::from_str(&json_text)?;
+        Ok(res)
+    }
+
+    pub fn get_partition(&mut self, name: &str) -> Option<&mut OtpImgPartition> {
+        self.partitions.iter_mut().find(|p| p.name == name)
+    }
+
+    pub fn partition(&self) -> &[OtpImgPartition] {
+        &self.partitions
+    }
+
+    pub fn get_rng(&self) -> StdRng {
+        StdRng::seed_from_u64(OTP_IMG_SEED_DIVERSIFIER + *self.seed)
+    }
+}
diff --git a/sw/host/opentitanlib/src/otp/otp_mmap.rs b/sw/host/opentitanlib/src/otp/otp_mmap.rs
new file mode 100644
index 0000000..e6960c3
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/otp_mmap.rs
@@ -0,0 +1,207 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::otp::num_de::{self, DeferredValue};
+use crate::otp::otp_img::OtpImg;
+use crate::otp::vmem_serialize::*;
+
+use anyhow::{anyhow, bail, Result};
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::convert::TryInto;
+use std::fs;
+use std::path::Path;
+
+#[derive(Deserialize, Debug)]
+struct OtpMapConfig {
+    #[serde(with = "num_de")]
+    width: usize,
+    #[serde(with = "num_de")]
+    depth: usize,
+}
+
+#[derive(Deserialize, Debug)]
+struct OtpMapKey {
+    name: String,
+    value: DeferredValue,
+}
+
+#[derive(Deserialize, Debug)]
+struct OtpMapDigest {
+    name: String,
+    iv_value: DeferredValue,
+    cnst_value: DeferredValue,
+}
+
+#[derive(Deserialize, Debug)]
+struct OtpMapScrambling {
+    #[serde(with = "num_de")]
+    key_size: usize,
+    #[serde(with = "num_de")]
+    iv_size: usize,
+    #[serde(with = "num_de")]
+    cnst_size: usize,
+    keys: Vec<OtpMapKey>,
+    digests: Vec<OtpMapDigest>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OtpMapItem {
+    name: String,
+    #[serde(with = "num_de")]
+    size: usize,
+    #[serde(default)]
+    isdigest: bool,
+    inv_default: Option<DeferredValue>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OtpMapPartition {
+    name: String,
+    secret: bool,
+    #[serde(default, with = "num_de")]
+    size: usize,
+    sw_digest: bool,
+    hw_digest: bool,
+    key_sel: String,
+    items: Vec<OtpMapItem>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OtpMap {
+    seed: String,
+    otp: OtpMapConfig,
+    scrambling: OtpMapScrambling,
+    partitions: Vec<OtpMapPartition>,
+}
+
+impl OtpMap {
+    pub fn new(in_file: &Path) -> Result<OtpMap> {
+        let json_text = fs::read_to_string(in_file)?;
+        let res: OtpMap = deser_hjson::from_str(&json_text)?;
+        Ok(res)
+    }
+
+    pub fn generate_keys(&self, img: &OtpImg) -> HashMap<String, Vec<u8>> {
+        let mut rng = img.get_rng();
+        let mut map = HashMap::new();
+        for key in &self.scrambling.keys {
+            let value = key.value.resolve(self.scrambling.key_size, &mut rng);
+            map.insert(key.name.clone(), value);
+        }
+        map
+    }
+
+    pub fn make_vmem(&mut self, img: &mut OtpImg) -> Result<VmemImage> {
+        // Seeded RNG needed for "<random>" values.
+        let mut rng = img.get_rng();
+        let mut vmem_partitions = Vec::<VmemPartition>::new();
+        for partition in &self.partitions {
+            let key_name = match partition.key_sel.as_str() {
+                "NoKey" => None,
+                key => Some(key.to_owned()),
+            };
+
+            let digest_type = if !partition.sw_digest && !partition.hw_digest {
+                DigestType::Unlocked
+            } else if partition.sw_digest && !partition.hw_digest {
+                DigestType::Software
+            } else if !partition.sw_digest && partition.hw_digest {
+                // Extra information needed to compute HW digests.
+                let iv_size = self.scrambling.iv_size;
+                let cnst_size = self.scrambling.cnst_size;
+                let digest_info = self
+                    .scrambling
+                    .digests
+                    .iter_mut()
+                    .find(|v| v.name == "CnstyDigest")
+                    .ok_or(anyhow!("Couldn't find digest info"))?;
+
+                const IV_SIZE: usize = std::mem::size_of::<DigestIV>();
+                const CNST_SIZE: usize = std::mem::size_of::<DigestCnst>();
+                let iv_value: [u8; IV_SIZE] = digest_info
+                    .iv_value
+                    .resolve(iv_size, &mut rng)
+                    .try_into()
+                    .map_err(|_| anyhow!("Bad IV size {}", iv_size))?;
+                let cnst_value: [u8; CNST_SIZE] = digest_info
+                    .cnst_value
+                    .resolve(cnst_size, &mut rng)
+                    .try_into()
+                    .map_err(|_| anyhow!("Bad scrambling constant size {}", cnst_size))?;
+                DigestType::Hardware(
+                    DigestIV::from_ne_bytes(iv_value),
+                    DigestCnst::from_ne_bytes(cnst_value),
+                )
+            } else {
+                bail!("Invalid digest configuration");
+            };
+
+            let mut vmem_partition = VmemPartition::new(
+                partition.name.clone(),
+                partition.size,
+                digest_type,
+                key_name,
+            );
+
+            // Fetch the img definition for partition, this contains the associated values for
+            // paritition items.
+            let mut img_partition = img.get_partition(&partition.name);
+
+            let mut offset = 0usize;
+
+            // Resolve all values and convert to Vmem representation.
+            for item in &partition.items {
+                let img_item_value = match &mut img_partition {
+                    Some(v) => {
+                        let item_value = v.get_item(&item.name);
+                        match item_value {
+                            Some(v) => v.value.resolve(item.size, &mut rng),
+                            None => vec![0u8; item.size],
+                        }
+                    }
+                    None => vec![0u8; item.size],
+                };
+                let vmem_item = VmemItem::new(img_item_value, offset, item.name.clone());
+                offset += item.size;
+                vmem_partition.push_item(vmem_item);
+            }
+            if partition.size == 0 {
+                const SCRAMBLE_BLOCK_WIDTH: usize = 8;
+                const DIGEST_SIZE: usize = 8;
+                let mut size = SCRAMBLE_BLOCK_WIDTH
+                    * ((offset + SCRAMBLE_BLOCK_WIDTH - 1) / SCRAMBLE_BLOCK_WIDTH);
+                if partition.hw_digest || partition.sw_digest {
+                    size += DIGEST_SIZE;
+                }
+                vmem_partition.set_size(size);
+            }
+            vmem_partitions.push(vmem_partition);
+        }
+        Ok(VmemImage::new(
+            vmem_partitions,
+            self.otp.width,
+            self.otp.depth,
+        ))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs::read_to_string;
+
+    #[test]
+    fn test_mmap_deserialize() {
+        let _: OtpMap =
+            deser_hjson::from_str(&read_to_string("tests/otp_ctrl_mmap.hjson").unwrap()).unwrap();
+    }
+
+    #[test]
+    fn test_img_deserialize() {
+        let _: OtpImg =
+            deser_hjson::from_str(&read_to_string("tests/otp_ctrl_img_dev.hjson").unwrap())
+                .unwrap();
+    }
+}
diff --git a/sw/host/opentitanlib/src/otp/vmem_serialize.rs b/sw/host/opentitanlib/src/otp/vmem_serialize.rs
new file mode 100644
index 0000000..077b46a
--- /dev/null
+++ b/sw/host/opentitanlib/src/otp/vmem_serialize.rs
@@ -0,0 +1,279 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::otp::lc_state::LcSecded;
+use crate::util::present::Present;
+
+use std::collections::HashMap;
+use std::convert::TryInto;
+use std::fmt::Write;
+
+use anyhow::{anyhow, bail, ensure, Result};
+
+use zerocopy::AsBytes;
+
+enum ItemType {
+    Bytes(Vec<u8>),
+    Unvalued(usize),
+}
+
+/// The hex representation of an OTP item.
+pub struct VmemItem {
+    value: ItemType,
+    offset: usize,
+    name: String,
+}
+
+impl VmemItem {
+    pub fn new(bytes: Vec<u8>, offset: usize, name: String) -> VmemItem {
+        VmemItem {
+            value: ItemType::Bytes(bytes),
+            offset,
+            name,
+        }
+    }
+
+    pub fn new_unvalued(size: usize, offset: usize, name: String) -> VmemItem {
+        VmemItem {
+            value: ItemType::Unvalued(size),
+            offset,
+            name,
+        }
+    }
+
+    pub fn size(&self) -> usize {
+        match &self.value {
+            ItemType::Bytes(b) => b.len(),
+            ItemType::Unvalued(size) => *size,
+        }
+    }
+}
+
+pub type DigestIV = u64;
+pub type DigestCnst = u128;
+
+/// Digest information for an OTP partition.
+#[derive(PartialEq)]
+pub enum DigestType {
+    Unlocked,
+    Software,
+    Hardware(DigestIV, DigestCnst),
+}
+
+/// The hex representation of an OTP partition.
+pub struct VmemPartition {
+    /// Items associated with this partition.
+    items: Vec<VmemItem>,
+    /// The name of this partition.
+    /// Used in annotations.
+    name: String,
+    /// The type of digest used for this partition.
+    /// For software digests, the value of the digest is provided and appended to the list of
+    /// items. For hardware digests, we must compute the digest value and append to the list of
+    /// items.
+    digest_type: DigestType,
+    /// Partition size.
+    size: usize,
+    /// The key name for this parition.
+    /// If specified, the serializer will attempt to scramble this parition using the key named in
+    /// this field.
+    key_name: Option<String>,
+}
+
+impl VmemPartition {
+    pub fn new(
+        name: String,
+        size: usize,
+        digest_type: DigestType,
+        key_name: Option<String>,
+    ) -> VmemPartition {
+        VmemPartition {
+            items: Vec::new(),
+            name,
+            digest_type,
+            size,
+            key_name,
+        }
+    }
+
+    /// Set the size of the partition.
+    ///
+    /// For partitions that don't specify their size, this is used to set the size of the partition
+    /// including the digest.
+    pub fn set_size(&mut self, size: usize) {
+        self.size = size;
+    }
+
+    /// Add an item to this partition.
+    pub fn push_item(&mut self, item: VmemItem) {
+        self.items.push(item);
+    }
+
+    /// Produces a tuple containing OTP HEX lines with annotations.
+    fn write_to_buffer(&self, keys: &HashMap<String, Vec<u8>>) -> Result<(Vec<u8>, Vec<String>)> {
+        if self.size % 8 != 0 {
+            bail!("Partition {} must be 64-bit alligned", self.name);
+        }
+
+        let mut defined = vec![false; self.size];
+        let mut annotations: Vec<String> = vec!["unallocated".to_owned(); self.size];
+
+        let mut data_bytes: Vec<u8> = vec![0; self.size];
+
+        for item in &self.items {
+            let end = item.offset + item.size();
+            annotations[item.offset..end].fill(format!("{}: {}", self.name, item.name).to_string());
+            let defined = &mut defined[item.offset..end];
+            if let Some(collision) = defined.iter().position(|defined| *defined) {
+                bail!(
+                    "Unexpected item collision with item {} at 0x{:x}",
+                    item.name,
+                    collision
+                );
+            }
+            defined.fill(true);
+            if let ItemType::Bytes(bytes) = &item.value {
+                data_bytes[item.offset..end].copy_from_slice(bytes);
+            }
+        }
+
+        let mut data_blocks = Vec::<u64>::new();
+        let mut data_blocks_defined = Vec::<bool>::new();
+        for (k, chunk) in data_bytes.chunks(8).enumerate() {
+            data_blocks.push(u64::from_le_bytes(chunk.try_into().unwrap()));
+            let byte_offset = k * 8;
+            data_blocks_defined.push(
+                defined[byte_offset..byte_offset + 8]
+                    .iter()
+                    .fold(false, |a, &b| a || b),
+            );
+        }
+
+        if let Some(key_name) = &self.key_name {
+            let key = keys
+                .get(key_name)
+                .ok_or_else(|| anyhow!("Key not found {}", key_name))?;
+
+            let cipher = Present::try_new(key.clone())?;
+
+            for i in 0..data_blocks.len() {
+                if data_blocks_defined[i] {
+                    data_blocks[i] = cipher.encrypt_block(data_blocks[i]);
+                }
+            }
+        }
+
+        if let DigestType::Hardware(iv, fin_const) = self.digest_type {
+            ensure!(
+                matches!(data_blocks.last(), None | Some(0)),
+                "Digest of partition {} cannot be overridden manually",
+                self.name
+            );
+            let last = data_blocks.len() - 1;
+            data_blocks[last] = present_digest_64(&data_blocks[0..last], iv, fin_const);
+        }
+
+        let data = data_blocks.as_bytes().to_vec();
+
+        if data.len() != self.size {
+            Err(anyhow!("Partition {} size mismatch", self.name))
+        } else {
+            Ok((data, annotations))
+        }
+    }
+}
+
+pub struct VmemImage {
+    partitions: Vec<VmemPartition>,
+    width: usize,
+    depth: usize,
+}
+
+impl VmemImage {
+    pub fn new(partitions: Vec<VmemPartition>, width: usize, depth: usize) -> VmemImage {
+        VmemImage {
+            partitions,
+            width,
+            depth,
+        }
+    }
+    pub fn generate(
+        &self,
+        keys: HashMap<String, Vec<u8>>,
+        secded: &LcSecded,
+    ) -> Result<Vec<String>> {
+        let mut data: Vec<u8> = vec![0; self.width * self.depth];
+        let mut annotations: Vec<String> = vec![Default::default(); data.len()];
+        let mut offset = 0;
+        for partition in &self.partitions {
+            let (part_data, part_annotation) = partition.write_to_buffer(&keys)?;
+            let end = offset + partition.size;
+            if end > data.len() {
+                bail!(
+                    "Partition {} out of bounds, ends at 0x{:x}",
+                    partition.name,
+                    end
+                );
+            }
+            data[offset..end].clone_from_slice(&part_data);
+            annotations[offset..end].clone_from_slice(&part_annotation);
+            offset += partition.size;
+        }
+
+        let width_ecc = self.width + secded.ecc_byte_len();
+        let num_words = data.len() / self.width;
+
+        let mut output = vec![format!(
+            "// OTP memory hexfile with {} x {}bit layout",
+            self.depth,
+            width_ecc * 8
+        )];
+
+        for i in 0..num_words {
+            let mut word = Vec::<u8>::new();
+            let mut word_annotation = Vec::<String>::new();
+            for j in 0..self.width {
+                let idx = i * self.width + j;
+                word.push(data[idx]);
+                if !word_annotation.contains(&annotations[idx]) {
+                    word_annotation.push(annotations[idx].clone());
+                }
+            }
+            let word_with_ecc = secded.ecc_encode(word)?;
+            let mut word_str = String::new();
+            for byte in word_with_ecc.iter().rev() {
+                write!(word_str, "{:02x}", byte)?;
+            }
+            output.push(format!(
+                "{} // {:06x}: {}",
+                word_str,
+                i * self.width,
+                word_annotation.join(", ")
+            ));
+        }
+
+        Ok(output)
+    }
+}
+
+fn present_digest_64(message: &[u64], iv: DigestIV, fin_const: DigestCnst) -> u64 {
+    let mut state = iv;
+    for i in (0..message.len() + 2).step_by(2) {
+        let b128: [u8; 16] = if i + 1 < message.len() {
+            (message[i] as u128) << 64 | message[i + 1] as u128
+        } else if i < message.len() {
+            (message[i] as u128) << 64 | message[i] as u128
+        } else {
+            fin_const
+        }
+        .as_bytes()
+        .try_into()
+        .unwrap();
+
+        let cipher = Present::new_128(&b128);
+        state ^= cipher.encrypt_block(state);
+    }
+
+    state
+}
diff --git a/sw/host/opentitanlib/src/util/mod.rs b/sw/host/opentitanlib/src/util/mod.rs
index 4d655de..1c3c7bf 100644
--- a/sw/host/opentitanlib/src/util/mod.rs
+++ b/sw/host/opentitanlib/src/util/mod.rs
@@ -7,8 +7,8 @@
 pub mod file;
 pub mod image;
 pub mod parse_int;
-pub mod usb;
 pub mod present;
+pub mod usb;
 pub mod voltage;
 
 /// The `collection` macro provides syntax for hash and set literals.
diff --git a/sw/host/opentitanlib/src/util/parse_int.rs b/sw/host/opentitanlib/src/util/parse_int.rs
index 1308013..6da3d48 100644
--- a/sw/host/opentitanlib/src/util/parse_int.rs
+++ b/sw/host/opentitanlib/src/util/parse_int.rs
@@ -73,6 +73,8 @@
 impl_parse_int!(u32);
 impl_parse_int!(i64);
 impl_parse_int!(u64);
+impl_parse_int!(i128);
+impl_parse_int!(u128);
 impl_parse_int!(isize);
 impl_parse_int!(usize);