Add initial support for parsing CSRs to hwtrust

The tool can now parse a CSR (both "bare" and "factory") and extract
the device info.

Future additions will include more complete parsing and validation.

Test: libhwtrust_tests
Bug: 280620556
Change-Id: I21dea4ed6e373232fe74ffd9342c13bf94011113
diff --git a/remote_provisioning/hwtrust/Android.bp b/remote_provisioning/hwtrust/Android.bp
index 64b10e9..69177f5 100644
--- a/remote_provisioning/hwtrust/Android.bp
+++ b/remote_provisioning/hwtrust/Android.bp
@@ -8,11 +8,13 @@
     srcs: ["src/lib.rs"],
     rustlibs: [
         "libanyhow",
-        "libthiserror",
+        "libbase64_rust",
         "libciborium",
         "libcoset",
         "libhex",
         "libopenssl",
+        "libserde_json",
+        "libthiserror",
     ],
 }
 
diff --git a/remote_provisioning/hwtrust/Cargo.lock b/remote_provisioning/hwtrust/Cargo.lock
index 12d91ba..de336b0 100644
--- a/remote_provisioning/hwtrust/Cargo.lock
+++ b/remote_provisioning/hwtrust/Cargo.lock
@@ -15,6 +15,12 @@
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+
+[[package]]
 name = "bitflags"
 version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -107,6 +113,12 @@
 ]
 
 [[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
 name = "errno"
 version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -174,11 +186,14 @@
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "base64",
  "ciborium",
  "clap",
  "coset",
  "hex",
+ "itertools",
  "openssl",
+ "serde_json",
  "thiserror",
 ]
 
@@ -205,6 +220,21 @@
 ]
 
 [[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+
+[[package]]
 name = "libc"
 version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -330,6 +360,12 @@
 ]
 
 [[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
+[[package]]
 name = "serde"
 version = "1.0.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -350,6 +386,17 @@
 ]
 
 [[package]]
+name = "serde_json"
+version = "1.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
 name = "strsim"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/remote_provisioning/hwtrust/Cargo.toml b/remote_provisioning/hwtrust/Cargo.toml
index 7d01144..b84ed05 100644
--- a/remote_provisioning/hwtrust/Cargo.toml
+++ b/remote_provisioning/hwtrust/Cargo.toml
@@ -15,4 +15,7 @@
 coset = "0.3.3"
 hex = "0.4.3"
 openssl = "0.10.45"
+serde_json = "1.0.96"
+itertools = "0.10.5"
+base64 = "0.21.0"
 
diff --git a/remote_provisioning/hwtrust/src/cbor.rs b/remote_provisioning/hwtrust/src/cbor.rs
index fafc6dd..8092506 100644
--- a/remote_provisioning/hwtrust/src/cbor.rs
+++ b/remote_provisioning/hwtrust/src/cbor.rs
@@ -3,7 +3,7 @@
 mod dice;
 mod field_value;
 mod publickey;
-mod rkp;
+pub(crate) mod rkp;
 
 use ciborium::{de::from_reader, value::Value};
 use std::io::Read;
diff --git a/remote_provisioning/hwtrust/src/cbor/field_value.rs b/remote_provisioning/hwtrust/src/cbor/field_value.rs
index ebf211e..63460a2 100644
--- a/remote_provisioning/hwtrust/src/cbor/field_value.rs
+++ b/remote_provisioning/hwtrust/src/cbor/field_value.rs
@@ -1,6 +1,6 @@
 //! This module defines a helper for parsing fields in a CBOR map.
 
-use coset::cbor::value::Value;
+use coset::{cbor::value::Value, AsCborValue, CoseError, CoseSign1};
 use thiserror::Error;
 
 #[derive(Error, Debug)]
@@ -21,6 +21,12 @@
     NotU64(&'static str, Value),
     #[error("expected boolean for field {0}, but found `{1:?}`")]
     NotBool(&'static str, Value),
+    #[error("expected map for field {0}, but found `{1:?}`")]
+    NotMap(&'static str, Value),
+    #[error("expected array for field {0}, but found `{1:?}`")]
+    NotArray(&'static str, Value),
+    #[error("unable to parse field {0} as COSE_sign1: `{1:?}`")]
+    CoseSign1ParseError(&'static str, CoseError),
     #[error("field {0} may be set only once; encountered multiple values: `{1:?}`, `{2:?}`")]
     DuplicateField(&'static str, Value, Value),
 }
@@ -35,6 +41,10 @@
         Self { name, value: None }
     }
 
+    pub fn from_optional_value(name: &'static str, value: Option<Value>) -> Self {
+        Self { name, value }
+    }
+
     pub fn set_once(&mut self, value: Value) -> Result<(), FieldValueError> {
         match &self.value {
             None => {
@@ -77,6 +87,33 @@
         require_present(self.name, self.into_optional_string())
     }
 
+    pub fn into_cose_sign1(self) -> Result<CoseSign1, FieldValueError> {
+        require_present(self.name, self.into_optional_cose_sign1())
+    }
+
+    pub fn into_optional_cose_sign1(self) -> Result<Option<CoseSign1>, FieldValueError> {
+        self.value
+            .map(|v| match v {
+                Value::Array(_) => CoseSign1::from_cbor_value(v)
+                    .map_err(|e| FieldValueError::CoseSign1ParseError(self.name, e)),
+                _ => Err(FieldValueError::NotArray(self.name, v)),
+            })
+            .transpose()
+    }
+
+    pub fn into_map(self) -> Result<Vec<(Value, Value)>, FieldValueError> {
+        require_present(self.name, self.into_optional_map())
+    }
+
+    pub fn into_optional_map(self) -> Result<Option<Vec<(Value, Value)>>, FieldValueError> {
+        self.value
+            .map(|v| match v {
+                Value::Map(v) => Ok(v),
+                _ => Err(FieldValueError::NotMap(self.name, v)),
+            })
+            .transpose()
+    }
+
     pub fn into_bool(self) -> Result<bool, FieldValueError> {
         require_present(self.name, self.into_optional_bool())
     }
diff --git a/remote_provisioning/hwtrust/src/cbor/rkp.rs b/remote_provisioning/hwtrust/src/cbor/rkp.rs
index 9fbec1c..3141707 100644
--- a/remote_provisioning/hwtrust/src/cbor/rkp.rs
+++ b/remote_provisioning/hwtrust/src/cbor/rkp.rs
@@ -1,3 +1,4 @@
 //! This module provides functions for dealing with RKP-specific data structures.
 
+pub(crate) mod csr;
 mod device_info;
diff --git a/remote_provisioning/hwtrust/src/cbor/rkp/csr.rs b/remote_provisioning/hwtrust/src/cbor/rkp/csr.rs
new file mode 100644
index 0000000..3c36fdd
--- /dev/null
+++ b/remote_provisioning/hwtrust/src/cbor/rkp/csr.rs
@@ -0,0 +1,161 @@
+use crate::cbor::field_value::FieldValue;
+use crate::rkp::{Csr, DeviceInfo};
+use crate::session::Session;
+use anyhow::{anyhow, bail, ensure, Context, Result};
+use base64::{prelude::BASE64_STANDARD, Engine};
+use ciborium::value::Value;
+
+const VERSION_OR_DEVICE_INFO_INDEX: usize = 0;
+
+impl Csr {
+    /// Parse base64-encoded CBOR data as a Certificate Signing Request.
+    pub fn from_base64_cbor<S: AsRef<[u8]>>(session: &Session, base64: &S) -> Result<Self> {
+        let cbor: Vec<u8> = BASE64_STANDARD.decode(base64).context("invalid base64 CSR")?;
+        Self::from_cbor(session, cbor.as_slice())
+    }
+
+    /// Read and parse CBOR data as a Certificate Signing Request.
+    pub fn from_cbor<S: std::io::Read>(session: &Session, cbor: S) -> Result<Self> {
+        let value: Value = ciborium::de::from_reader(cbor).context("invalid CBOR")?;
+        let mut array = match value {
+            Value::Array(a) if a.is_empty() => bail!("CSR CBOR is an empty array"),
+            Value::Array(a) => a,
+            other => bail!("expected array, found {other:?}"),
+        };
+        let version_or_device_info =
+            std::mem::replace(&mut array[VERSION_OR_DEVICE_INFO_INDEX], Value::Null);
+        match version_or_device_info {
+            Value::Array(device_info) => Self::v2_from_cbor_values(session, array, device_info),
+            Value::Integer(i) => Self::v3_from_authenticated_request(session, array, i.into()),
+            other => Err(anyhow!(
+                "Expected integer or array at index {VERSION_OR_DEVICE_INFO_INDEX}, \
+                found {other:?}"
+            )),
+        }
+    }
+
+    fn v2_from_cbor_values(
+        _session: &Session,
+        _csr: Vec<Value>,
+        mut device_info: Vec<Value>,
+    ) -> Result<Self> {
+        ensure!(device_info.len() == 2, "Device info should contain exactly 2 entries");
+        device_info.pop(); // ignore unverified info
+        let verified_device_info = match device_info.pop() {
+            Some(Value::Map(d)) => d,
+            other => bail!("Expected a map for verified device info, found '{:?}'", other),
+        };
+
+        Ok(Self::V2 { device_info: DeviceInfo::from_cbor_values(verified_device_info, None)? })
+    }
+
+    fn v3_from_authenticated_request(
+        _session: &Session,
+        mut csr: Vec<Value>,
+        version: i128,
+    ) -> Result<Self> {
+        if version != 1 {
+            bail!("Invalid CSR version. Only '1' is supported, found '{}", version);
+        }
+
+        let _unverified_info = FieldValue::from_optional_value("UnverifiedDeviceInfo", csr.pop());
+
+        let signed_data =
+            FieldValue::from_optional_value("SignedData", csr.pop()).into_cose_sign1()?;
+        let signed_data_payload = signed_data.payload.context("missing payload in SignedData")?;
+        let csr_payload_value =
+            ciborium::de::from_reader::<Value, &[u8]>(signed_data_payload.as_slice())
+                .context("SignedData payload is not valid CBOR")?
+                .as_array_mut()
+                .context("SignedData payload is not a CBOR array")?
+                .pop()
+                .context("Missing CsrPayload in SignedData")?;
+        let csr_payload_bytes = csr_payload_value
+            .as_bytes()
+            .context("CsrPayload (in SignedData) is expected to be encoded CBOR")?
+            .as_slice();
+        let mut csr_payload = match ciborium::de::from_reader(csr_payload_bytes)? {
+            Value::Array(a) => a,
+            other => bail!("CsrPayload is expected to be an array, found {other:?}"),
+        };
+
+        let _keys_to_sign = FieldValue::from_optional_value("KeysToSign", csr_payload.pop());
+        let device_info = FieldValue::from_optional_value("DeviceInfo", csr_payload.pop());
+        let _certificate_type =
+            FieldValue::from_optional_value("CertificateType", csr_payload.pop());
+
+        let device_info = DeviceInfo::from_cbor_values(device_info.into_map()?, Some(3))?;
+        Ok(Self::V3 { device_info })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    // More complete testing happens in the factorycsr module, as the test data
+    // generation spits out full JSON files, not just a CSR. Therefore, only a
+    // minimal number of smoke tests are here.
+    use super::*;
+    use crate::rkp::DeviceInfoVersion;
+    use std::fs;
+
+    #[test]
+    fn from_base64_valid_v2() {
+        let input = fs::read_to_string("testdata/csr/v2_csr.base64").unwrap().trim().to_owned();
+        let csr = Csr::from_base64_cbor(&Session::default(), &input).unwrap();
+        assert_eq!(csr, Csr::V2 { device_info: testutil::test_device_info(DeviceInfoVersion::V2) });
+    }
+
+    #[test]
+    fn from_base64_valid_v3() {
+        let input = fs::read_to_string("testdata/csr/v3_csr.base64").unwrap().trim().to_owned();
+        let csr = Csr::from_base64_cbor(&Session::default(), &input).unwrap();
+        assert_eq!(csr, Csr::V3 { device_info: testutil::test_device_info(DeviceInfoVersion::V3) });
+    }
+
+    #[test]
+    fn from_empty_string() {
+        let err = Csr::from_base64_cbor(&Session::default(), &"").unwrap_err();
+        assert!(err.to_string().contains("invalid CBOR"));
+    }
+
+    #[test]
+    fn from_garbage() {
+        let err = Csr::from_base64_cbor(&Session::default(), &"cnViYmlzaAo=").unwrap_err();
+        assert!(err.to_string().contains("invalid CBOR"));
+    }
+
+    #[test]
+    fn from_invalid_base64() {
+        let err = Csr::from_base64_cbor(&Session::default(), &"not base64").unwrap_err();
+        assert!(err.to_string().contains("invalid base64"));
+    }
+}
+
+#[cfg(test)]
+pub(crate) mod testutil {
+    use crate::rkp::{
+        DeviceInfo, DeviceInfoBootloaderState, DeviceInfoSecurityLevel, DeviceInfoVbState,
+        DeviceInfoVersion,
+    };
+
+    // The test data uses mostly common DeviceInfo fields
+    pub fn test_device_info(version: DeviceInfoVersion) -> DeviceInfo {
+        DeviceInfo {
+            version,
+            brand: "Google".to_string(),
+            manufacturer: "Google".to_string(),
+            product: "pixel".to_string(),
+            model: "model".to_string(),
+            device: "device".to_string(),
+            vb_state: DeviceInfoVbState::Green,
+            bootloader_state: DeviceInfoBootloaderState::Locked,
+            vbmeta_digest: b"\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff".to_vec(),
+            os_version: Some("12".to_string()),
+            system_patch_level: 20221025,
+            boot_patch_level: 20221026,
+            vendor_patch_level: 20221027,
+            security_level: Some(DeviceInfoSecurityLevel::Tee),
+            fused: true,
+        }
+    }
+}
diff --git a/remote_provisioning/hwtrust/src/rkp.rs b/remote_provisioning/hwtrust/src/rkp.rs
index 048b434..efb512f 100644
--- a/remote_provisioning/hwtrust/src/rkp.rs
+++ b/remote_provisioning/hwtrust/src/rkp.rs
@@ -1,8 +1,14 @@
 //! This module contains data types used in Remote Key Provisioning.
 
+mod csr;
 mod device_info;
+mod factory_csr;
+
+pub use csr::Csr;
 
 pub use device_info::{
     DeviceInfo, DeviceInfoBootloaderState, DeviceInfoSecurityLevel, DeviceInfoVbState,
     DeviceInfoVersion,
 };
+
+pub use factory_csr::FactoryCsr;
diff --git a/remote_provisioning/hwtrust/src/rkp/csr.rs b/remote_provisioning/hwtrust/src/rkp/csr.rs
new file mode 100644
index 0000000..8e538b2
--- /dev/null
+++ b/remote_provisioning/hwtrust/src/rkp/csr.rs
@@ -0,0 +1,20 @@
+use crate::rkp::DeviceInfo;
+
+/// Represents a Certificate Signing Request that is sent to an RKP backend to request
+/// certificates to be signed for a set of public keys. The CSR is partially generated by an
+/// IRemotelyProvisionedComponent HAL. The set of public keys to be signed is authenticated
+/// (signed) with a device-unique key.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Csr {
+    /// CSR V2 was introduced in Android T. In this version, the payload is encrypted using
+    /// an Endpoint Encryption Key (EEK).
+    V2 {
+        /// Describes the device that is requesting certificates.
+        device_info: DeviceInfo,
+    },
+    /// CSR V3 was introduced in Android T. This version drops encryption of the payload.
+    V3 {
+        /// Describes the device that is requesting certificates.
+        device_info: DeviceInfo,
+    },
+}
diff --git a/remote_provisioning/hwtrust/src/rkp/device_info.rs b/remote_provisioning/hwtrust/src/rkp/device_info.rs
index ae7a71e..64ae695 100644
--- a/remote_provisioning/hwtrust/src/rkp/device_info.rs
+++ b/remote_provisioning/hwtrust/src/rkp/device_info.rs
@@ -1,7 +1,9 @@
 use anyhow::anyhow;
+use hex;
+use std::fmt;
 
 #[non_exhaustive]
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Eq, PartialEq)]
 /// Describes a device that is registered with the RKP backend. This implementation contains fields
 /// common to all versions defined in DeviceInfo.aidl.
 pub struct DeviceInfo {
@@ -39,6 +41,31 @@
     pub fused: bool,
 }
 
+impl fmt::Debug for DeviceInfo {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        let security_level: &dyn fmt::Debug = self.security_level.as_ref().map_or(&"<none>", |s| s);
+        let os_version: &dyn fmt::Debug = self.os_version.as_ref().map_or(&"<none>", |v| v);
+
+        fmt.debug_struct("DeviceInfo")
+            .field("version", &self.version)
+            .field("brand", &self.brand)
+            .field("manufacturer", &self.manufacturer)
+            .field("product", &self.product)
+            .field("model", &self.model)
+            .field("device", &self.device)
+            .field("vb_state", &self.vb_state)
+            .field("bootloader_state", &self.bootloader_state)
+            .field("vbmeta_digest", &hex::encode(&self.vbmeta_digest))
+            .field("os_version", os_version)
+            .field("system_patch_level", &self.system_patch_level)
+            .field("boot_patch_level", &self.boot_patch_level)
+            .field("vendor_patch_level", &self.vendor_patch_level)
+            .field("security_level", security_level)
+            .field("fused", &self.fused)
+            .finish()
+    }
+}
+
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 /// Whether the bootloader allows unsigned or third-party-signed images.
 pub enum DeviceInfoBootloaderState {
diff --git a/remote_provisioning/hwtrust/src/rkp/factory_csr.rs b/remote_provisioning/hwtrust/src/rkp/factory_csr.rs
new file mode 100644
index 0000000..10136b3
--- /dev/null
+++ b/remote_provisioning/hwtrust/src/rkp/factory_csr.rs
@@ -0,0 +1,156 @@
+use crate::rkp::Csr;
+use crate::session::Session;
+use anyhow::{bail, Result};
+use serde_json::{Map, Value};
+
+/// Represents a "Factory CSR", which is a JSON value captured for each device on the factory
+/// line. This JSON is uploaded to the RKP backend to register the device. We reuse the CSR
+/// (Certificate Signing Request) format for this as an implementation convenience. The CSR
+/// actually contains an empty set of keys for which certificates are needed.
+#[non_exhaustive]
+#[derive(Debug, Eq, PartialEq)]
+pub struct FactoryCsr {
+    /// The CSR, as created by an IRemotelyProvisionedComponent HAL.
+    pub csr: Csr,
+    /// The name of the HAL that generated the CSR.
+    pub name: String,
+}
+
+fn get_string_from_map(fields: &Map<String, Value>, key: &str) -> Result<String> {
+    match fields.get(key) {
+        Some(Value::String(s)) => Ok(s.to_string()),
+        Some(v) => bail!("Unexpected type for '{key}'. Expected String, found '{v:?}'"),
+        None => bail!("Unable to locate '{key}' in input"),
+    }
+}
+
+impl FactoryCsr {
+    /// Parse the input JSON string into a CSR that was captured on the factory line. The
+    /// format of the JSON data is defined by rkp_factory_extraction_tool.
+    pub fn from_json(session: &Session, json: &str) -> Result<Self> {
+        match serde_json::from_str(json) {
+            Ok(Value::Object(map)) => Self::from_map(session, map),
+            Ok(unexpected) => bail!("Expected a map, got some other type: {unexpected}"),
+            Err(e) => bail!("Error parsing input json: {e}"),
+        }
+    }
+
+    fn from_map(session: &Session, fields: Map<String, Value>) -> Result<Self> {
+        let base64 = get_string_from_map(&fields, "csr")?;
+        let name = get_string_from_map(&fields, "name")?;
+        let csr = Csr::from_base64_cbor(session, &base64)?;
+        Ok(Self { csr, name })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::cbor::rkp::csr::testutil::test_device_info;
+    use crate::rkp::device_info::DeviceInfoVersion;
+    use crate::rkp::factory_csr::FactoryCsr;
+    use anyhow::anyhow;
+    use std::fs;
+    use std::fs::File;
+
+    fn json_map_from_file(path: &str) -> Result<Map<String, Value>> {
+        let input = File::open(path)?;
+        match serde_json::from_reader(input)? {
+            Value::Object(map) => Ok(map),
+            other => Err(anyhow!("Unexpected JSON. Wanted a map, found {other:?}")),
+        }
+    }
+
+    #[test]
+    fn from_json_valid_v2_ed25519() {
+        let json = fs::read_to_string("testdata/factory_csr/v2_ed25519_valid.json").unwrap();
+        let csr = FactoryCsr::from_json(&Session::default(), &json).unwrap();
+        assert_eq!(
+            csr,
+            FactoryCsr {
+                csr: Csr::V2 { device_info: test_device_info(DeviceInfoVersion::V2) },
+                name: "default".to_string(),
+            }
+        );
+    }
+
+    #[test]
+    fn from_json_valid_v3_ed25519() {
+        let json = fs::read_to_string("testdata/factory_csr/v3_ed25519_valid.json").unwrap();
+        let csr = FactoryCsr::from_json(&Session::default(), &json).unwrap();
+        assert_eq!(
+            csr,
+            FactoryCsr {
+                csr: Csr::V3 { device_info: test_device_info(DeviceInfoVersion::V3) },
+                name: "default".to_string(),
+            }
+        );
+    }
+
+    #[test]
+    fn from_json_valid_v2_p256() {
+        let json = fs::read_to_string("testdata/factory_csr/v2_p256_valid.json").unwrap();
+        let csr = FactoryCsr::from_json(&Session::default(), &json).unwrap();
+        assert_eq!(
+            csr,
+            FactoryCsr {
+                csr: Csr::V2 { device_info: test_device_info(DeviceInfoVersion::V2) },
+                name: "default".to_string(),
+            }
+        );
+    }
+
+    #[test]
+    fn from_json_valid_v3_p256() {
+        let json = fs::read_to_string("testdata/factory_csr/v3_p256_valid.json").unwrap();
+        let csr = FactoryCsr::from_json(&Session::default(), &json).unwrap();
+        assert_eq!(
+            csr,
+            FactoryCsr {
+                csr: Csr::V3 { device_info: test_device_info(DeviceInfoVersion::V3) },
+                name: "default".to_string(),
+            }
+        );
+    }
+
+    #[test]
+    fn from_json_name_is_missing() {
+        let mut value = json_map_from_file("testdata/factory_csr/v2_ed25519_valid.json").unwrap();
+        value.remove_entry("name");
+        let json = serde_json::to_string(&value).unwrap();
+        let err = FactoryCsr::from_json(&Session::default(), &json).unwrap_err();
+        assert!(err.to_string().contains("Unable to locate 'name'"));
+    }
+
+    #[test]
+    fn from_json_name_is_wrong_type() {
+        let mut value = json_map_from_file("testdata/factory_csr/v2_ed25519_valid.json").unwrap();
+        value.insert("name".to_string(), Value::Object(Map::default()));
+        let json = serde_json::to_string(&value).unwrap();
+        let err = FactoryCsr::from_json(&Session::default(), &json).unwrap_err();
+        assert!(err.to_string().contains("Unexpected type for 'name'"));
+    }
+
+    #[test]
+    fn from_json_csr_is_missing() {
+        let json = r#"{ "name": "default" }"#;
+        let err = FactoryCsr::from_json(&Session::default(), json).unwrap_err();
+        assert!(err.to_string().contains("Unable to locate 'csr'"));
+    }
+
+    #[test]
+    fn from_json_csr_is_wrong_type() {
+        let json = r#"{ "csr": 3.1415, "name": "default" }"#;
+        let err = FactoryCsr::from_json(&Session::default(), json).unwrap_err();
+        assert!(err.to_string().contains("Unexpected type for 'csr'"));
+    }
+
+    #[test]
+    fn from_json_extra_tag_is_ignored() {
+        let mut value = json_map_from_file("testdata/factory_csr/v2_ed25519_valid.json").unwrap();
+        value.insert("extra".to_string(), Value::Bool(true));
+        let json = serde_json::to_string(&value).unwrap();
+        let csr = FactoryCsr::from_json(&Session::default(), &json).unwrap();
+        assert_eq!(csr.name, "default");
+    }
+}
diff --git a/remote_provisioning/hwtrust/src/session.rs b/remote_provisioning/hwtrust/src/session.rs
index 180860d..db39731 100644
--- a/remote_provisioning/hwtrust/src/session.rs
+++ b/remote_provisioning/hwtrust/src/session.rs
@@ -1,6 +1,7 @@
 //! Defines the context type for a session handling hwtrust data structures.
 
 /// The context for a session handling hwtrust data structures.
+#[derive(Default)]
 pub struct Session {
     /// Options that control the behaviour during this session.
     pub options: Options,
diff --git a/remote_provisioning/hwtrust/testdata/csr/v2_csr.base64 b/remote_provisioning/hwtrust/testdata/csr/v2_csr.base64
new file mode 100644
index 0000000..2ef4a8f
--- /dev/null
+++ b/remote_provisioning/hwtrust/testdata/csr/v2_csr.base64
@@ -0,0 +1 @@
+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
diff --git a/remote_provisioning/hwtrust/testdata/csr/v3_csr.base64 b/remote_provisioning/hwtrust/testdata/csr/v3_csr.base64
new file mode 100644
index 0000000..adf1e66
--- /dev/null
+++ b/remote_provisioning/hwtrust/testdata/csr/v3_csr.base64
@@ -0,0 +1 @@
+hQGggqYBAgMmIAEhWCCQbWgM4qcVbKaD/OV+shAQv3yZVEua9250IFP/ua8tkSJYIJvbeqNvlUh6taXlPmAnqamH2oSIt642HwEoXgddAluII1ggHtpjixl6qMLpEH9AWTb/3oefocW+EmA8qPxVLiI2xdiEQ6EBJqBYxagBZmlzc3VlcgJnc3ViamVjdDoAR0RQQwABAjoAR0RTVaE6AAERcW5jb21wb25lbnRfbmFtZToAR0RUQwABAjoAR0RWQQE6AEdEV1hxpgECAyYgASFYIKrrkKhyQly2CYBqrIAxLehLwW8RZUI7EyRYFb/o40rCIlggcQmkixd5yLkzQ/kI/jjet9Kzc9YhcC2gVbZiI9+1YQ8jWCEAiBXy6pC0zpGzFKUQLOjZ0rSNfQtUvXimMwa7JMFwkbk6AEdEWEEgWEDwei8KDsy64HBKZS9BUpzsHO9+pBG0XVEjeuGi/gXccZnGW5FrHuLHrZ1CP3cEtvR2KjjHJtZk9BL8l+sNYAfbhEOhASagWQIPglggAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyBZAemEA2dLRVlNSU5UrmVicmFuZGZHb29nbGVlZnVzZWQBZW1vZGVsZW1vZGVsZmRldmljZWZkZXZpY2VncHJvZHVjdGVwaXhlbGh2Yl9zdGF0ZWVncmVlbmpvc192ZXJzaW9uYjEybG1hbnVmYWN0dXJlcmZHb29nbGVtdmJtZXRhX2RpZ2VzdE8RIjNEVWZ3iJmqu8zd7v9uc2VjdXJpdHlfbGV2ZWxjdGVlcGJvb3RfcGF0Y2hfbGV2ZWwaATSMYnBib290bG9hZGVyX3N0YXRlZmxvY2tlZHJzeXN0ZW1fcGF0Y2hfbGV2ZWwaATSMYXJ2ZW5kb3JfcGF0Y2hfbGV2ZWwaATSMY4KmAQIDJiABIVggQAfcLLxViAxXadWDUn0IJN2QmF8qrRKo26sQNMATmKgiWCCXL67UZ0V+d0jJoKyBOXbaScvu1A02TEZlLZXf5uSr7SNYIAqXQk/RpXA7Gt3QOSdZqz7AlXGCXN4BvQe16jEd7l3fpgECAyYgASFYIBZ/iS7yreG89/IaVWW5SQPvGK6ijryCyltOOw8rUfR+IlggqN0J7ivOB3dKitFcF6GeJgD5snOwBKnoYwwwYWBGDl8jWCEAh3YQNQDm4iK4oT3EwnYkOAjK5iwDzCve9YVUTJfUIyhYQHunfdCq9s8TvyTZ6j1iHNz/pux98QIZpJlnDkrehpazteRjeicPJXdiSyNXhsThYhVIm4rcI5XgkBNi5mz3PXqha2ZpbmdlcnByaW50eDticmFuZDEvcHJvZHVjdDEvZGV2aWNlMToxMS9pZC8yMDIxMDgwNS40Mjp1c2VyL3JlbGVhc2Uta2V5cw==
diff --git a/remote_provisioning/hwtrust/testdata/factory_csr/v2_ed25519_valid.json b/remote_provisioning/hwtrust/testdata/factory_csr/v2_ed25519_valid.json
new file mode 100644
index 0000000..a27fb42
--- /dev/null
+++ b/remote_provisioning/hwtrust/testdata/factory_csr/v2_ed25519_valid.json
@@ -0,0 +1 @@
+{"csr":"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","name":"default","serialno":"fake-device-0"}
diff --git a/remote_provisioning/hwtrust/testdata/factory_csr/v2_p256_valid.json b/remote_provisioning/hwtrust/testdata/factory_csr/v2_p256_valid.json
new file mode 100644
index 0000000..f010e55
--- /dev/null
+++ b/remote_provisioning/hwtrust/testdata/factory_csr/v2_p256_valid.json
@@ -0,0 +1 @@
+{"csr":"hIKvZWJyYW5kZkdvb2dsZWVmdXNlZAFlbW9kZWxlbW9kZWxmZGV2aWNlZmRldmljZWdwcm9kdWN0ZXBpeGVsZ3ZlcnNpb24CaHZiX3N0YXRlZWdyZWVuam9zX3ZlcnNpb25iMTJsbWFudWZhY3R1cmVyZkdvb2dsZW12Ym1ldGFfZGlnZXN0TxEiM0RVZneImaq7zN3u/25zZWN1cml0eV9sZXZlbGN0ZWVwYm9vdF9wYXRjaF9sZXZlbBoBNIxicGJvb3Rsb2FkZXJfc3RhdGVmbG9ja2VkcnN5c3RlbV9wYXRjaF9sZXZlbBoBNIxhcnZlbmRvcl9wYXRjaF9sZXZlbBoBNIxjoFABAgMEBQYHCAkKCwwNDg8QhEOhAQOhBVB8SGVJT/kGHh6S+uozCl07WQH657MN4GJCirkP+lHYcuPS538uvOXlh3fiUVchQmcYlYndP+/va2BI3vboQj/dHBmop/UILHuX0q3syjBmm0eMbOleFh7KFQVZRib7mbG8wgoDIE+jghkVYGDm2iszr438seyE+MDZRwxI4gkPzrBXEkkyN0X+LvLz6ANlpuNAdeyaGO/3reoB+cPNWNrBB28Ldx+3kd3Jp4V08tGp2m+KOJgNqHy2Y5dCRvLX87CpXphe2H+DNXou93E5JTePnGrSzZdBHHPhE/q+or6uLZdS7v7xMbUvClQv2ZmkIPMxWJAyWgHdjONT63f9EWfrk8v5eE/thfDGPrfQPdvQvOlrh8oUmr1FoNe8WaBrZqVpm81Dr939nQxGl+Wk0X6uXRXl0fhzQoxZr4bYXW/6tvz3NrEme/v7CUhOtb4N7r1pOQZ31n0umaBSoC5GTiRixVYUpL4KB2JyFLqfxwhHPB4wrCzgxpEslfsopE0VHb+iPR7ibBWN6Wc7EJ4n9e85YkGrrjKXviEUlqWwsL1MXSkXCh8gKMFg3CVlennDdAhw/DbAKF8vQwP32A9rxRZ2KiM521mejSNyQBfwuleA0BraBg8p4OuXOvRBcf2ZTkd8/AKcnj7aHGaNseA+noJFAofyGkAMLwbZc7ttCGaJ/gyH0pUD/Lap1XEt1/GBg0ShATgYogRYIDVztz+gioCJsSZn6ct8daGvAmH8bmUDkTvTS30UlD5GIKQBAiABIVggrXoMDRADqmefBvftkTG9uXoV+oX+ZJvdZGIrhUPPdXciWCCrnXSWOkVVJTCjex9u622rADn2AjMAXY9sihTeH2Iji/aEQ6EBBaBYl4KkAQIgASFYIH31wzJNpqIWZPeoBUxlz7LJRSco5bUo4D7FRbHYcaZfIlggpAmS/zsYSSfXNkrxj7AKexd41VcPTTM/YtcGnedm1jakAQIgASFYIMy0g5IMFEJOihApibR1Lk8Adb8uT2YyApbMNhqMlxqiIlggUM9q/Lp6/ylLzNbFnYQMaa1ygdZTTpb1wmb1xV9LDcBYIPQrdGZIzk8dengNOxVFAngM6/TuNidXJZY2M6c2+LGm","name":"default","serialno":"fake-device-0"}
diff --git a/remote_provisioning/hwtrust/testdata/factory_csr/v3_ed25519_valid.json b/remote_provisioning/hwtrust/testdata/factory_csr/v3_ed25519_valid.json
new file mode 100644
index 0000000..58aae72
--- /dev/null
+++ b/remote_provisioning/hwtrust/testdata/factory_csr/v3_ed25519_valid.json
@@ -0,0 +1 @@
+{"csr":"hQGggqUBAQMnIAYhWCAsuQh3dEqvFh388CNmcs/EF27rN3EbYEOvu/puokB6WiNYIC6HHSDrS0Qvur+/sw1Wv4c3wmODHnqsoQZm7lBpv4w7hEOhASegWMWoAWZpc3N1ZXICZ3N1YmplY3Q6AEdEUEMAAQI6AEdEU1WhOgABEXFuY29tcG9uZW50X25hbWU6AEdEVEMAAQI6AEdEVkEBOgBHRFdYcaYBAgMmIAEhWCBi/AYDJ9hQnqfcGCyY46kV2IKa7zZGt3vV2utH2Y7VByJYIFKXej1PFrBMc0zJTYgmU5+fWba6eOPQWqpqYE+iQmswI1ghAPxi72oVN9U2BFPI4ezP38FrdSurJsxY5O0/Wvr6sdk/OgBHRFhBIFhAcNXSblkgb1tI3KkLV2hv8jF7HDBTrmbk2Zgtbf+GjrlIDzwIKIYvdssmzwkFT5kq7+X1vtMqzdBsVHgQBkNYBIRDoQEmoFkCD4JYIAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gWQHphANnS0VZTUlOVK5lYnJhbmRmR29vZ2xlZWZ1c2VkAWVtb2RlbGVtb2RlbGZkZXZpY2VmZGV2aWNlZ3Byb2R1Y3RlcGl4ZWxodmJfc3RhdGVlZ3JlZW5qb3NfdmVyc2lvbmIxMmxtYW51ZmFjdHVyZXJmR29vZ2xlbXZibWV0YV9kaWdlc3RPESIzRFVmd4iZqrvM3e7/bnNlY3VyaXR5X2xldmVsY3RlZXBib290X3BhdGNoX2xldmVsGgE0jGJwYm9vdGxvYWRlcl9zdGF0ZWZsb2NrZWRyc3lzdGVtX3BhdGNoX2xldmVsGgE0jGFydmVuZG9yX3BhdGNoX2xldmVsGgE0jGOCpgECAyYgASFYICibs6BzzvTC2P6fFbOs4hIEF2E5YyOgJL06ogIGO2VhIlggikGxp7d8r2mMqG2GCy4zu82QmdxTbxzEj7oZrnRtuIkjWCEAowwtl6x4fDYXXKxPGUVFsM5h/fshUTlFs5zls2+sYS6mAQIDJiABIVggrG7CFpYoMKNmtqK1F5ERG4jB9rcvS1KxuZe7V/TDgvUiWCAQtNZDZd78RioZLQvqTKYT2ib7qh4u/2CQbfWVIhz9uSNYIDWD6q2AC+F1oDQjkIpKiXD9ZqyenMEXhbr+5DV7szBEWECW3sFhmIg9YKmw/nVersPrkikWN80U2JDUK4+ZpVg8wljyxj5tgslvkRJJutE6N9dGUzWX8wGBGmNUNxr4XlQUoWtmaW5nZXJwcmludHg7YnJhbmQxL3Byb2R1Y3QxL2RldmljZTE6MTEvaWQvMjAyMTA4MDUuNDI6dXNlci9yZWxlYXNlLWtleXM=","name":"default","serialno":"fake-device-0"}
diff --git a/remote_provisioning/hwtrust/testdata/factory_csr/v3_p256_valid.json b/remote_provisioning/hwtrust/testdata/factory_csr/v3_p256_valid.json
new file mode 100644
index 0000000..2593669
--- /dev/null
+++ b/remote_provisioning/hwtrust/testdata/factory_csr/v3_p256_valid.json
@@ -0,0 +1 @@
+{"csr":"hQGggqYBAgMmIAEhWCCQbWgM4qcVbKaD/OV+shAQv3yZVEua9250IFP/ua8tkSJYIJvbeqNvlUh6taXlPmAnqamH2oSIt642HwEoXgddAluII1ggHtpjixl6qMLpEH9AWTb/3oefocW+EmA8qPxVLiI2xdiEQ6EBJqBYxagBZmlzc3VlcgJnc3ViamVjdDoAR0RQQwABAjoAR0RTVaE6AAERcW5jb21wb25lbnRfbmFtZToAR0RUQwABAjoAR0RWQQE6AEdEV1hxpgECAyYgASFYIKrrkKhyQly2CYBqrIAxLehLwW8RZUI7EyRYFb/o40rCIlggcQmkixd5yLkzQ/kI/jjet9Kzc9YhcC2gVbZiI9+1YQ8jWCEAiBXy6pC0zpGzFKUQLOjZ0rSNfQtUvXimMwa7JMFwkbk6AEdEWEEgWEDwei8KDsy64HBKZS9BUpzsHO9+pBG0XVEjeuGi/gXccZnGW5FrHuLHrZ1CP3cEtvR2KjjHJtZk9BL8l+sNYAfbhEOhASagWQIPglggAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyBZAemEA2dLRVlNSU5UrmVicmFuZGZHb29nbGVlZnVzZWQBZW1vZGVsZW1vZGVsZmRldmljZWZkZXZpY2VncHJvZHVjdGVwaXhlbGh2Yl9zdGF0ZWVncmVlbmpvc192ZXJzaW9uYjEybG1hbnVmYWN0dXJlcmZHb29nbGVtdmJtZXRhX2RpZ2VzdE8RIjNEVWZ3iJmqu8zd7v9uc2VjdXJpdHlfbGV2ZWxjdGVlcGJvb3RfcGF0Y2hfbGV2ZWwaATSMYnBib290bG9hZGVyX3N0YXRlZmxvY2tlZHJzeXN0ZW1fcGF0Y2hfbGV2ZWwaATSMYXJ2ZW5kb3JfcGF0Y2hfbGV2ZWwaATSMY4KmAQIDJiABIVggQAfcLLxViAxXadWDUn0IJN2QmF8qrRKo26sQNMATmKgiWCCXL67UZ0V+d0jJoKyBOXbaScvu1A02TEZlLZXf5uSr7SNYIAqXQk/RpXA7Gt3QOSdZqz7AlXGCXN4BvQe16jEd7l3fpgECAyYgASFYIBZ/iS7yreG89/IaVWW5SQPvGK6ijryCyltOOw8rUfR+IlggqN0J7ivOB3dKitFcF6GeJgD5snOwBKnoYwwwYWBGDl8jWCEAh3YQNQDm4iK4oT3EwnYkOAjK5iwDzCve9YVUTJfUIyhYQHunfdCq9s8TvyTZ6j1iHNz/pux98QIZpJlnDkrehpazteRjeicPJXdiSyNXhsThYhVIm4rcI5XgkBNi5mz3PXqha2ZpbmdlcnByaW50eDticmFuZDEvcHJvZHVjdDEvZGV2aWNlMToxMS9pZC8yMDIxMDgwNS40Mjp1c2VyL3JlbGVhc2Uta2V5cw==","name":"default","serialno":"fake-device-0"}