ot_certs/template/
mod.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5//! OpenTitan certificate template deserialization.
6//!
7//! This module contains structs defining certificate templates.
8//!
9//! These structs are defined in Hjson files and deserialized to here. Any
10//! extra conversion required (beyond simple renaming) is done in the `hjson`
11//! module.
12//!
13//! The format for a template in Hjson looks something like:
14//!
15//! ```hjson
16//! {
17//!   variables: {
18//!     SomeVariableName: {
19//!       type: "byte-array",
20//!       size: 20,
21//!     },
22//!     // ...
23//!   },
24//!
25//!   certificate: {
26//!     // Certificate keys, some making use of variables, others not.
27//!     serial_number: { var: "SomeVariableName" },
28//!     layer: 0,
29//!     // ...
30//!   }
31//! }
32//! ```
33
34use anyhow::Result;
35use indexmap::IndexMap;
36use num_bigint_dig::BigUint;
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38
39pub mod subst;
40pub mod testgen;
41
42use crate::template::subst::{ConvertValue, SubstValue};
43
44/// Full template file, including variable declarations and certificate spec.
45#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
46#[serde(deny_unknown_fields)]
47pub struct Template {
48    /// Name of the certificate.
49    pub name: String,
50    /// Variable declarations.
51    pub variables: IndexMap<String, VariableType>,
52    /// Certificate specification.
53    pub certificate: Certificate,
54}
55
56/// Certificate specification.
57#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
58#[serde(deny_unknown_fields)]
59pub struct Certificate {
60    /// X509 certificate's serial number
61    pub serial_number: Value<BigUint>,
62    /// X509 validity's not before date. The format must be a valid ASN1 GeneralizedTime.
63    pub not_before: Value<String>,
64    /// X509 validity's not after date. The format must be a valid ASN1 GeneralizedTime.
65    pub not_after: Value<String>,
66    /// X509 certificate's issuer.
67    pub issuer: Name,
68    /// X509 certificate's subject.
69    pub subject: Name,
70    /// X509 certificate's public key.
71    pub subject_public_key_info: SubjectPublicKeyInfo,
72    /// X509 certificate's authority key identifier.
73    pub authority_key_identifier: Option<Value<Vec<u8>>>,
74    /// X509 certificate's public key identifier.
75    pub subject_key_identifier: Option<Value<Vec<u8>>>,
76    // X509 basic constraints extension, optional.
77    pub basic_constraints: Option<BasicConstraints>,
78    pub key_usage: Option<KeyUsage>,
79    /// X509 Subject Alternative Name extension, optional.
80    #[serde(default)]
81    pub subject_alt_name: Name,
82    /// Non-standard X509 certificate extensions.
83    #[serde(default)]
84    pub private_extensions: Vec<CertificateExtension>,
85    /// X509 certificate's signature.
86    pub signature: Signature,
87}
88
89/// An X501 Name (or DistinguishedName, aka DN): a DN consists of a sequence of
90/// RelativeDistinguishedName (RDN). An RDN is an ordered set of attribute type
91/// and value pairs. Within an RDN, each attribute type can only appear once.
92/// Therefore, we represent a name as a vector of RDN, and each RDN is represented
93/// by a map. The order of the vector is important: changing the order changes
94/// the name. The order within the map is not important but we use an `IndexMap`
95/// to make the consumers of this template use a deterministic order.
96pub type Name = Vec<IndexMap<AttributeType, Value<String>>>;
97
98#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
99pub struct BasicConstraints {
100    pub ca: Value<bool>,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
104pub struct KeyUsage {
105    pub digital_signature: Option<Value<bool>>,
106    pub key_agreement: Option<Value<bool>>,
107    pub cert_sign: Option<Value<bool>>,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
111#[serde(tag = "type", rename_all = "snake_case")]
112pub enum CertificateExtension {
113    /// DICE TCB extension.
114    DiceTcbInfo(DiceTcbInfoExtension),
115}
116
117/// DICE TCB extension.
118#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
119#[serde(deny_unknown_fields)]
120pub struct DiceTcbInfoExtension {
121    /// TCB model.
122    pub model: Option<Value<String>>,
123    /// TCB vendor.
124    pub vendor: Option<Value<String>>,
125    /// TCB version.
126    pub version: Option<Value<String>>,
127    /// TCB security version number.
128    pub svn: Option<Value<BigUint>>,
129    /// TCB layer.
130    pub layer: Option<Value<BigUint>>,
131    /// TCB firmware IDs.
132    pub fw_ids: Option<Vec<FirmwareId>>,
133    /// TCB flags.
134    pub flags: Option<DiceTcbInfoFlags>,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Hash, strum::Display, Serialize)]
138#[serde(rename_all = "snake_case")]
139pub enum AttributeType {
140    #[serde(alias = "c")]
141    Country,
142    #[serde(alias = "o")]
143    Organization,
144    #[serde(alias = "ou")]
145    OrganizationalUnit,
146    #[serde(alias = "st")]
147    State,
148    #[serde(alias = "cn")]
149    CommonName,
150    #[serde(alias = "sn")]
151    SerialNumber,
152    TpmVendor,
153    TpmModel,
154    TpmVersion,
155}
156
157/// Value which may either be a variable name or literal.
158#[derive(Clone, Debug, PartialEq, Eq)]
159pub enum Value<T> {
160    /// This value will be populated on the device when variables are set.
161    Variable(Variable),
162    /// Constant literal that will be set when the certificate is generated.
163    Literal(T),
164}
165
166/// Value which may either be a variable name or literal.
167#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
168#[serde(deny_unknown_fields)]
169pub struct Variable {
170    /// Name of the variable.
171    pub name: String,
172    /// Optional conversion to apply to the variable.
173    pub convert: Option<Conversion>,
174}
175
176impl<T> Value<T> {
177    /// Create a variable with the given name. No conversion applied.
178    pub fn variable(name: &str) -> Self {
179        Value::Variable(Variable {
180            name: name.into(),
181            convert: None,
182        })
183    }
184
185    /// Create a variable with the given name and conversion.
186    pub fn convert(var: &str, conversion: Conversion) -> Self {
187        Value::Variable(Variable {
188            name: var.into(),
189            convert: Some(conversion),
190        })
191    }
192
193    /// Create a literal with the given value.
194    pub fn literal(value: impl Into<T>) -> Self {
195        Value::Literal(value.into())
196    }
197
198    /// Return true if the value is a literal
199    pub fn is_literal(&self) -> bool {
200        matches!(self, Self::Literal(_))
201    }
202
203    /// Return true if this value is a variable that refers to `var_name`.
204    pub fn refers_to(&self, var_name: &str) -> bool {
205        match self {
206            Value::Literal(_) => false,
207            Value::Variable(Variable { name, .. }) => name == var_name,
208        }
209    }
210}
211
212// Manual implementation of the deserializer for Value<T> to call into `subst` that
213// handles the parsing.
214impl<'de, T> Deserialize<'de> for Value<T>
215where
216    T: Deserialize<'de>,
217    SubstValue: ConvertValue<T>,
218{
219    fn deserialize<D>(deserializer: D) -> Result<Value<T>, D::Error>
220    where
221        D: Deserializer<'de>,
222    {
223        #[derive(Deserialize)]
224        #[serde(untagged)]
225        pub enum LocalValue {
226            Variable {
227                var: String,
228                convert: Option<Conversion>,
229            },
230            Literal(SubstValue),
231        }
232        match LocalValue::deserialize(deserializer) {
233            Ok(val) => match val {
234                LocalValue::Literal(raw_val) => {
235                    let val = raw_val.convert(&None).map_err(serde::de::Error::custom)?;
236                    Ok(Value::<T>::Literal(val))
237                }
238                LocalValue::Variable { var, convert, .. } => {
239                    Ok(Value::<T>::Variable(Variable { name: var, convert }))
240                }
241            },
242            Err(_) => {
243                let msg = "could not parse value: expected either a literal (string, integer or array of bytes) or a variable (use the syntax {{var: \"name\"}}";
244                Err(serde::de::Error::custom(msg))
245            }
246        }
247    }
248}
249
250// Manual implementation of the serializer for Value<T> to call into `subst` that
251// handles the serializing.
252impl<T> Serialize for Value<T>
253where
254    SubstValue: ConvertValue<T>,
255{
256    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
257    where
258        S: Serializer,
259    {
260        #[derive(Serialize)]
261        #[serde(untagged)]
262        pub enum LocalValue {
263            Variable {
264                var: String,
265                convert: Option<Conversion>,
266            },
267            Literal(SubstValue),
268        }
269        let res = match self {
270            Value::Variable(Variable { name, convert }) => LocalValue::Variable {
271                var: name.clone(),
272                convert: *convert,
273            },
274            Value::Literal(x) => {
275                LocalValue::Literal(SubstValue::unconvert(x).map_err(serde::ser::Error::custom)?)
276            }
277        };
278        res.serialize(serializer)
279    }
280}
281
282/// Conversion to apply to a variable when inserting it into the certificate.
283#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
284#[serde(rename_all = "kebab-case")]
285pub enum Conversion {
286    /// Lower case hex: convert a byte array to a string in lowercase
287    /// hexadecimal form. Every byte is printed using exactly two characters
288    /// and there is no "0x" prefix. Example:
289    /// [42, 53] -> "2a35".
290    LowercaseHex,
291    /// Big endian: convert between a byte array and integer in big-endian format.
292    BigEndian,
293}
294
295/// Representation of the signature of the certificate.
296#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
297#[serde(tag = "algorithm", rename_all = "kebab-case")]
298pub enum Signature {
299    EcdsaWithSha256 { value: Option<EcdsaSignature> },
300}
301
302/// Representation of an ECDSA signature.
303///
304/// The signature consists of two integers "r" and "s".
305/// See X9.62
306#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
307#[serde(deny_unknown_fields)]
308pub struct EcdsaSignature {
309    pub r: Value<BigUint>,
310    pub s: Value<BigUint>,
311}
312
313/// Representation of the `SubjectPublicKeyInfo` field.
314#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
315#[serde(tag = "algorithm", rename_all = "kebab-case")]
316pub enum SubjectPublicKeyInfo {
317    EcPublicKey(EcPublicKeyInfo),
318}
319
320/// Representation of an elliptic curve public key information.
321#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
322#[serde(deny_unknown_fields)]
323pub struct EcPublicKeyInfo {
324    pub curve: EcCurve,
325    pub public_key: EcPublicKey,
326}
327
328/// Representation of an elliptic curve public key in uncompressed
329/// form.
330#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
331#[serde(deny_unknown_fields)]
332pub struct EcPublicKey {
333    pub x: Value<BigUint>,
334    pub y: Value<BigUint>,
335}
336
337/// List of EC named curves.
338#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
339pub enum EcCurve {
340    #[serde(rename = "prime256v1")]
341    Prime256v1,
342}
343
344/// Flags that can be set for a certificate.
345#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
346#[serde(deny_unknown_fields)]
347pub struct DiceTcbInfoFlags {
348    pub not_configured: Value<bool>,
349    pub not_secure: Value<bool>,
350    pub recovery: Value<bool>,
351    pub debug: Value<bool>,
352}
353
354/// Firmware ID (fwid) field.
355#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
356#[serde(deny_unknown_fields)]
357pub struct FirmwareId {
358    /// Algorithm used for the has of the firmware.
359    pub hash_algorithm: HashAlgorithm,
360    /// Raw bytes of the hashed firmware.
361    pub digest: Value<Vec<u8>>,
362}
363
364/// Possible algorithms for computing hashes.
365#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
366pub enum HashAlgorithm {
367    #[serde(rename = "sha256")]
368    Sha256,
369}
370
371/// SizeRange sets the range of the variable it represented.
372#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
373#[serde(rename_all = "kebab-case")]
374pub enum SizeRange {
375    /// The [min, max] size of the variable in bytes.
376    RangeSize(usize, usize),
377    /// Equivalent to RangeSize(size, size).
378    ExactSize(usize),
379}
380
381impl SizeRange {
382    pub fn range(self) -> (usize, usize) {
383        match self {
384            Self::RangeSize(min_size, max_size) => (min_size, max_size),
385            Self::ExactSize(size) => (size, size),
386        }
387    }
388}
389
390/// Declaration of a variable that can be filled into the template.
391#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
392#[serde(tag = "type", rename_all = "kebab-case")]
393pub enum VariableType {
394    /// Raw array of bytes.
395    #[serde(rename_all = "kebab-case")]
396    ByteArray {
397        #[serde(flatten)]
398        size: SizeRange,
399
400        /// The MSb will always be set when encoded as an integer.
401        tweak_msb: Option<bool>,
402    },
403    /// Signed integer: such an integer is represented by an array of
404    /// in big-endian.
405    Integer {
406        #[serde(flatten)]
407        size: SizeRange,
408    },
409    /// UTF-8 encoded String.
410    String {
411        #[serde(flatten)]
412        size: SizeRange,
413    },
414    /// Boolean variable.
415    Boolean,
416}
417
418impl VariableType {
419    /// Return the maximum array size of the variable.
420    pub fn size(&self) -> usize {
421        self.array_size().1
422    }
423
424    /// Return true if the variable uses msb tweak trick.
425    pub fn use_msb_tweak(&self) -> bool {
426        use VariableType::*;
427        matches!(
428            self,
429            ByteArray {
430                tweak_msb: Some(true),
431                ..
432            }
433        )
434    }
435
436    /// Return true if the user guarantees to pass a fixed-length array for
437    /// this variable.
438    pub fn has_constant_array_size(&self) -> bool {
439        let (min_size, max_size) = self.array_size();
440        min_size == max_size
441    }
442
443    /// Return the the user's guarantee on the array size passing for this
444    /// variable.
445    ///
446    /// The result is the closed range [min, max].
447    pub fn array_size(&self) -> (usize, usize) {
448        use VariableType::*;
449        match self {
450            ByteArray { size, .. } | String { size, .. } => size.range(),
451            // The array buffer for integer should always have the maximum size.
452            Integer { size, .. } => (size.range().1, size.range().1),
453            Boolean => panic!("Boolean variable has no array size"),
454        }
455    }
456
457    /// Return the the user's guarantee on the size of the integer value
458    /// represented by the u8 array.
459    ///
460    /// `extra_bytes` argument specifies the amount of extra bytes that
461    /// will be added when the MSb is set.
462    ///
463    /// The result is the closed range [min, max].
464    pub fn int_size(&self, extra_bytes: usize) -> (usize, usize) {
465        use VariableType::*;
466        match self {
467            ByteArray {
468                tweak_msb: Some(true),
469                ..
470            } => {
471                if !self.has_constant_array_size() {
472                    panic!("Tweak MSb of var-sized ByteArray is not supported");
473                }
474                (self.size() + extra_bytes, self.size() + extra_bytes)
475            }
476            ByteArray { .. } => panic!(
477                "Encoding ByteArray variable without tweak-msb as an integer is not supported"
478            ),
479            Integer { size, .. } => (size.range().0, size.range().1 + extra_bytes),
480            String { .. } => panic!("String variable has no integer size"),
481            Boolean => panic!("Boolean variable has no integer size"),
482        }
483    }
484}
485
486impl Template {
487    pub fn from_hjson_str(content: &str) -> Result<Template> {
488        Ok(deser_hjson::from_str(content)?)
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use indoc::indoc;
496
497    /// Test parsing a typical cdi_owner template.
498    #[test]
499    fn cdi_owner() {
500        use SizeRange::*;
501
502        // Input string for a Hjson template.
503        let input = indoc! {r#"
504            {
505              name: "cdi_owner",
506
507              variables: {
508                owner_pub_key_ec_x: {
509                  type: "integer",
510                  exact-size: 32,
511                },
512                owner_pub_key_ec_y: {
513                  type: "integer",
514                  exact-size: 32,
515                },
516                owner_pub_key_id: {
517                  type: "byte-array",
518                  exact-size: 20,
519                  tweak-msb: true
520                },
521                signing_pub_key_id: {
522                  type: "byte-array",
523                  exact-size: 20,
524                  tweak-msb: true
525                },
526                rom_ext_hash: {
527                  type: "byte-array",
528                  exact-size: 20,
529                },
530                ownership_manifest_hash: {
531                  type: "byte-array",
532                  exact-size: 20,
533                },
534                rom_ext_security_version: {
535                  type: "byte-array",
536                  exact-size: 4,
537                  tweak-msb: true,
538                }
539                layer: {
540                  type: "integer",
541                  range-size: [1, 4],
542                }
543                cert_signature_r: {
544                  type: "integer",
545                  range-size: [24, 32],
546                },
547                cert_signature_s: {
548                  type: "integer",
549                  range-size: [24, 32],
550                },
551              },
552
553              certificate: {
554                serial_number: { var: "owner_pub_key_id", convert: "big-endian" },
555                issuer: [
556                  { serial_number: { var: "signing_pub_key_id", convert: "lowercase-hex" } },
557                ],
558                not_before: "20230101000000Z",
559                not_after: "99991231235959Z",
560                subject: [
561                  { serial_number: { var: "owner_pub_key_id", convert: "lowercase-hex" } },
562                ],
563                subject_public_key_info: {
564                  algorithm: "ec-public-key",
565                  curve: "prime256v1",
566                  public_key: {
567                    x: { var: "owner_pub_key_ec_x" },
568                    y: { var: "owner_pub_key_ec_y" },
569                  },
570                },
571                authority_key_identifier: { var: "signing_pub_key_id" },
572                subject_key_identifier: { var: "owner_pub_key_id" },
573                key_usage: { key_agreement: true },
574                private_extensions: [
575                    {
576                        type: "dice_tcb_info",
577                        vendor: "OpenTitan",
578                        model: "ROM_EXT",
579                        svn: { var: "rom_ext_security_version", convert: "big-endian" },
580                        layer: { var: "layer" },
581                        version: "ES",
582                        fw_ids: [
583                        { hash_algorithm: "sha256", digest: { var: "rom_ext_hash" } },
584                        { hash_algorithm: "sha256", digest: { var: "ownership_manifest_hash" } },
585                        ],
586                        flags: {
587                        not_configured: true,
588                        not_secure: false,
589                        recovery: true,
590                        debug: false,
591                        }
592                    },
593                ],
594                signature: {
595                  algorithm: "ecdsa-with-sha256",
596                  // The value field is optional: if not present, the signature will be cleared.
597                  // Otherwise, we can reference the various fields of the signature.
598                  value: {
599                    r: { var: "cert_signature_r" },
600                    s: { var: "cert_signature_s" }
601                  }
602                }
603              }
604            }
605        "#};
606
607        let variables = IndexMap::from([
608            (
609                "owner_pub_key_ec_x".to_string(),
610                VariableType::Integer {
611                    size: ExactSize(32),
612                },
613            ),
614            (
615                "owner_pub_key_ec_y".to_string(),
616                VariableType::Integer {
617                    size: ExactSize(32),
618                },
619            ),
620            (
621                "owner_pub_key_id".to_string(),
622                VariableType::ByteArray {
623                    size: ExactSize(20),
624                    tweak_msb: Some(true),
625                },
626            ),
627            (
628                "signing_pub_key_id".to_string(),
629                VariableType::ByteArray {
630                    size: ExactSize(20),
631                    tweak_msb: Some(true),
632                },
633            ),
634            (
635                "rom_ext_hash".to_string(),
636                VariableType::ByteArray {
637                    size: ExactSize(20),
638                    tweak_msb: None,
639                },
640            ),
641            (
642                "ownership_manifest_hash".to_string(),
643                VariableType::ByteArray {
644                    size: ExactSize(20),
645                    tweak_msb: None,
646                },
647            ),
648            (
649                "rom_ext_security_version".to_string(),
650                VariableType::ByteArray {
651                    size: ExactSize(4),
652                    tweak_msb: Some(true),
653                },
654            ),
655            (
656                "layer".to_string(),
657                VariableType::Integer {
658                    size: RangeSize(1, 4),
659                },
660            ),
661            (
662                "cert_signature_r".to_string(),
663                VariableType::Integer {
664                    size: RangeSize(24, 32),
665                },
666            ),
667            (
668                "cert_signature_s".to_string(),
669                VariableType::Integer {
670                    size: RangeSize(24, 32),
671                },
672            ),
673        ]);
674
675        // Certificate template values.
676        let certificate = Certificate {
677            serial_number: Value::convert("owner_pub_key_id", Conversion::BigEndian),
678            issuer: vec![IndexMap::from([(
679                AttributeType::SerialNumber,
680                Value::convert("signing_pub_key_id", Conversion::LowercaseHex),
681            )])],
682            not_before: Value::literal("20230101000000Z"),
683            not_after: Value::literal("99991231235959Z"),
684            subject: vec![IndexMap::from([(
685                AttributeType::SerialNumber,
686                Value::convert("owner_pub_key_id", Conversion::LowercaseHex),
687            )])],
688            subject_public_key_info: SubjectPublicKeyInfo::EcPublicKey(EcPublicKeyInfo {
689                curve: EcCurve::Prime256v1,
690                public_key: EcPublicKey {
691                    x: Value::variable("owner_pub_key_ec_x"),
692                    y: Value::variable("owner_pub_key_ec_y"),
693                },
694            }),
695            authority_key_identifier: Some(Value::variable("signing_pub_key_id")),
696            subject_key_identifier: Some(Value::variable("owner_pub_key_id")),
697            basic_constraints: None,
698            key_usage: Some(KeyUsage {
699                digital_signature: None,
700                key_agreement: Some(Value::literal(true)),
701                cert_sign: None,
702            }),
703            subject_alt_name: vec![],
704            private_extensions: vec![CertificateExtension::DiceTcbInfo(DiceTcbInfoExtension {
705                vendor: Some(Value::literal("OpenTitan")),
706                model: Some(Value::literal("ROM_EXT")),
707                svn: Some(Value::convert(
708                    "rom_ext_security_version",
709                    Conversion::BigEndian,
710                )),
711                layer: Some(Value::variable("layer")),
712                version: Some(Value::literal("ES")),
713                fw_ids: Some(Vec::from([
714                    FirmwareId {
715                        hash_algorithm: HashAlgorithm::Sha256,
716                        digest: Value::variable("rom_ext_hash"),
717                    },
718                    FirmwareId {
719                        hash_algorithm: HashAlgorithm::Sha256,
720                        digest: Value::variable("ownership_manifest_hash"),
721                    },
722                ])),
723                flags: Some(DiceTcbInfoFlags {
724                    not_configured: Value::Literal(true),
725                    not_secure: Value::Literal(false),
726                    recovery: Value::Literal(true),
727                    debug: Value::Literal(false),
728                }),
729            })],
730            signature: Signature::EcdsaWithSha256 {
731                value: Some(EcdsaSignature {
732                    r: Value::variable("cert_signature_r"),
733                    s: Value::variable("cert_signature_s"),
734                }),
735            },
736        };
737
738        // Compare expected and actual parsed structs.
739        let expected = Template {
740            name: "cdi_owner".to_string(),
741            variables,
742            certificate,
743        };
744        let actual = Template::from_hjson_str(input).expect("failed to parse template");
745        // Manual assertion for pretty-printing the huge output if necessary.
746        assert_eq!(
747            expected, actual,
748            "certificate mismatch: expected {expected:#?} but got {actual:#?}"
749        );
750    }
751}