summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/builtins/build.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /security/manager/ssl/builtins/build.rs
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--security/manager/ssl/builtins/build.rs568
1 files changed, 568 insertions, 0 deletions
diff --git a/security/manager/ssl/builtins/build.rs b/security/manager/ssl/builtins/build.rs
new file mode 100644
index 0000000000..8375f3368d
--- /dev/null
+++ b/security/manager/ssl/builtins/build.rs
@@ -0,0 +1,568 @@
+/* -*- Mode: rust; rust-indent-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate bindgen;
+extern crate nom;
+
+use bindgen::callbacks::*;
+use bindgen::*;
+
+use nom::branch::alt;
+use nom::bytes::complete::{tag, take_until};
+use nom::character::complete::{
+ char, multispace0, newline, not_line_ending, one_of, space0, space1,
+};
+use nom::combinator::{fail, recognize};
+use nom::multi::{many1, separated_list0};
+use nom::sequence::{delimited, separated_pair, terminated, tuple};
+use nom::IResult;
+
+use std::collections::HashMap;
+use std::env;
+use std::fmt;
+use std::fs::File;
+use std::io::{BufWriter, Write};
+use std::path::PathBuf;
+
+fn octal_block_to_vec_u8(octal_block: &str) -> Vec<u8> {
+ octal_block
+ .lines()
+ .flat_map(|x| x.split('\\').skip(1))
+ .map(|x| u8::from_str_radix(x, 8).expect("octal value out of range."))
+ .collect()
+}
+
+fn octal_block_to_hex_string(octal: &str) -> String {
+ octal_block_to_vec_u8(octal)
+ .iter()
+ .map(|x| format!("0x{:02X}, ", x))
+ .collect()
+}
+
+// Wrapper around values parsed out of certdata.txt
+enum Ck<'a> {
+ Class(&'a str),
+ Comment(&'a str),
+ DistrustAfter(Option<&'a str>),
+ Empty,
+ MultilineOctal(&'a str),
+ OptionBool(&'a str),
+ Trust(&'a str),
+ Utf8(&'a str),
+}
+
+// Translation of parsed values into the output rust code
+impl fmt::Display for Ck<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Ck::Class(s) => write!(f, "{s}_BYTES"),
+ Ck::Comment(s) => write!(f, "{}", s.replace('#', "//")),
+ Ck::DistrustAfter(None) => write!(f, "Some(CK_FALSE_BYTES)"),
+ Ck::DistrustAfter(Some(s)) => write!(f, "Some(&[{}])", octal_block_to_hex_string(s)),
+ Ck::Empty => write!(f, "None"),
+ Ck::MultilineOctal(s) => write!(f, "&[{}]", octal_block_to_hex_string(s)),
+ Ck::OptionBool(s) => write!(f, "Some({s}_BYTES)"),
+ Ck::Trust(s) => write!(f, "{s}_BYTES"),
+ Ck::Utf8(s) => write!(f, "\"{s}\\0\""),
+ }
+ }
+}
+
+impl PartialEq for Ck<'_> {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (Ck::Class(s), Ck::Class(t)) => s.eq(t),
+ (Ck::Comment(s), Ck::Comment(t)) => s.eq(t),
+ (Ck::DistrustAfter(None), Ck::DistrustAfter(None)) => true,
+ (Ck::DistrustAfter(Some(s)), Ck::DistrustAfter(Some(t))) => {
+ // compare the data rather than the presentation
+ let vec_s = octal_block_to_vec_u8(s);
+ let vec_t = octal_block_to_vec_u8(t);
+ vec_s.eq(&vec_t)
+ }
+ (Ck::Empty, Ck::Empty) => true,
+ (Ck::MultilineOctal(s), Ck::MultilineOctal(t)) => {
+ // compare the data rather than the presentation
+ let vec_s = octal_block_to_vec_u8(s);
+ let vec_t = octal_block_to_vec_u8(t);
+ vec_s.eq(&vec_t)
+ }
+ (Ck::Trust(s), Ck::Trust(t)) => s.eq(t),
+ (Ck::Utf8(s), Ck::Utf8(t)) => s.eq(t),
+ _ => false,
+ }
+ }
+}
+
+fn class(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("CK_OBJECT_CLASS")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, class) = alt((
+ tag("CKO_NSS_BUILTIN_ROOT_LIST"),
+ tag("CKO_CERTIFICATE"),
+ tag("CKO_NSS_TRUST"),
+ ))(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::Class(class)))
+}
+
+fn trust(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("CK_TRUST")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, trust) = alt((
+ tag("CKT_NSS_TRUSTED_DELEGATOR"),
+ tag("CKT_NSS_MUST_VERIFY_TRUST"),
+ tag("CKT_NSS_NOT_TRUSTED"),
+ ))(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::Trust(trust)))
+}
+
+// Parses a CK_BBOOL and wraps it with Ck::OptionBool so that it gets printed as
+// "Some(CK_TRUE_BYTES)" instead of "CK_TRUE_BYTES".
+fn option_bbool(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("CK_BBOOL")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, b) = alt((tag("CK_TRUE"), tag("CK_FALSE")))(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::OptionBool(b)))
+}
+
+fn bbool_true(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("CK_BBOOL")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, _) = tag("CK_TRUE")(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::Empty))
+}
+
+fn bbool_false(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("CK_BBOOL")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, _) = tag("CK_FALSE")(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::Empty))
+}
+
+fn utf8(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("UTF8")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, _) = char('"')(i)?;
+ let (i, utf8) = take_until("\"")(i)?;
+ let (i, _) = char('"')(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::Utf8(utf8)))
+}
+
+fn certificate_type(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("CK_CERTIFICATE_TYPE")(i)?;
+ let (i, _) = space1(i)?;
+ let (i, _) = tag("CKC_X_509")(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ Ok((i, Ck::Empty))
+}
+
+// A CKA_NSS_{EMAIL,SERVER}_DISTRUST_AFTER line in certdata.txt is encoded either as a CK_BBOOL
+// with value CK_FALSE (when there is no distrust after date) or as a MULTILINE_OCTAL block.
+fn distrust_after(i: &str) -> IResult<&str, Ck> {
+ let (i, value) = alt((multiline_octal, bbool_false))(i)?;
+ match value {
+ Ck::Empty => Ok((i, Ck::DistrustAfter(None))),
+ Ck::MultilineOctal(data) => Ok((i, Ck::DistrustAfter(Some(data)))),
+ _ => unreachable!(),
+ }
+}
+
+fn octal_octet(i: &str) -> IResult<&str, &str> {
+ recognize(tuple((
+ tag("\\"),
+ one_of("0123"), // 255 = \377
+ one_of("01234567"),
+ one_of("01234567"),
+ )))(i)
+}
+
+fn multiline_octal(i: &str) -> IResult<&str, Ck> {
+ let (i, _) = tag("MULTILINE_OCTAL")(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ let (i, lines) = recognize(many1(terminated(many1(octal_octet), newline)))(i)?;
+ let (i, _) = tag("END")(i)?;
+ let (i, _) = space0(i)?;
+ let (i, _) = newline(i)?;
+ return Ok((i, Ck::MultilineOctal(lines)));
+}
+
+fn distrust_comment(i: &str) -> IResult<&str, (&str, Ck)> {
+ let (i, comment) = recognize(delimited(
+ alt((
+ tag("# For Email Distrust After: "),
+ tag("# For Server Distrust After: "),
+ )),
+ not_line_ending,
+ newline,
+ ))(i)?;
+ Ok((i, ("DISTRUST_COMMENT", Ck::Comment(comment))))
+}
+
+fn comment(i: &str) -> IResult<&str, (&str, Ck)> {
+ let (i, comment) = recognize(many1(delimited(char('#'), not_line_ending, newline)))(i)?;
+ Ok((i, ("COMMENT", Ck::Comment(comment))))
+}
+
+fn certdata_line(i: &str) -> IResult<&str, (&str, Ck)> {
+ let (i, (attr, value)) = alt((
+ distrust_comment, // must be listed before `comment`
+ comment,
+ separated_pair(tag("CKA_CLASS"), space1, class),
+ separated_pair(tag("CKA_CERTIFICATE_TYPE"), space1, certificate_type),
+ separated_pair(alt((tag("CKA_ID"), tag("CKA_LABEL"))), space1, utf8),
+ separated_pair(
+ alt((
+ tag("CKA_ISSUER"),
+ tag("CKA_CERT_SHA1_HASH"),
+ tag("CKA_CERT_MD5_HASH"),
+ tag("CKA_SERIAL_NUMBER"),
+ tag("CKA_SUBJECT"),
+ tag("CKA_VALUE"),
+ )),
+ space1,
+ multiline_octal,
+ ),
+ separated_pair(
+ alt((
+ tag("CKA_NSS_SERVER_DISTRUST_AFTER"),
+ tag("CKA_NSS_EMAIL_DISTRUST_AFTER"),
+ )),
+ space1,
+ distrust_after,
+ ),
+ separated_pair(
+ alt((
+ tag("CKA_TRUST_EMAIL_PROTECTION"),
+ tag("CKA_TRUST_CODE_SIGNING"),
+ tag("CKA_TRUST_SERVER_AUTH"),
+ )),
+ space1,
+ trust,
+ ),
+ separated_pair(tag("CKA_NSS_MOZILLA_CA_POLICY"), space1, option_bbool),
+ separated_pair(tag("CKA_TOKEN"), space1, bbool_true),
+ separated_pair(
+ alt((
+ tag("CKA_TRUST_STEP_UP_APPROVED"),
+ tag("CKA_PRIVATE"),
+ tag("CKA_MODIFIABLE"),
+ )),
+ space1,
+ bbool_false,
+ ),
+ ))(i)?;
+ Ok((i, (attr, value)))
+}
+
+type Block<'a> = HashMap<&'a str, Ck<'a>>;
+
+fn attr<'a>(block: &'a Block, attr: &str) -> &'a Ck<'a> {
+ block.get(attr).unwrap_or(&Ck::Empty)
+}
+
+fn parse(i: &str) -> IResult<&str, Vec<Block>> {
+ let mut out: Vec<Block> = vec![];
+ let (i, _) = take_until("BEGINDATA\n")(i)?;
+ let (i, _) = tag("BEGINDATA\n")(i)?;
+ let (i, mut raw_blocks) = separated_list0(many1(char('\n')), many1(certdata_line))(i)?;
+ let (i, _) = multispace0(i)?; // allow trailing whitespace
+ if !i.is_empty() {
+ // The first line of i contains an error.
+ let (line, _) = i.split_once('\n').unwrap_or((i, ""));
+ fail::<_, &str, _>(line)?;
+ }
+ for raw_block in raw_blocks.drain(..) {
+ out.push(raw_block.into_iter().collect())
+ }
+ Ok((i, out))
+}
+
+#[derive(Debug)]
+struct PKCS11TypesParseCallbacks;
+
+impl ParseCallbacks for PKCS11TypesParseCallbacks {
+ fn int_macro(&self, _name: &str, _value: i64) -> Option<IntKind> {
+ Some(IntKind::U8)
+ }
+}
+
+// If we encounter a problem parsing certdata.txt we'll try to turn it into a compile time
+// error in builtins.rs. We need to output definitions for ROOT_LIST_LABEL and BUILTINS to
+// cut down on the number of errors the compiler produces.
+macro_rules! emit_build_error {
+ ($out:ident, $err:expr) => {
+ writeln!($out, "std::compile_error!(\"{}\");", $err)?;
+ writeln!($out, "pub static ROOT_LIST_LABEL: [u8; 0] = [];")?;
+ writeln!($out, "pub static BUILTINS: [Root; 0] = [];")?;
+ };
+}
+
+fn main() -> std::io::Result<()> {
+ println!("cargo:rerun-if-changed=../../../nss/lib/ckfw/builtins/certdata.txt");
+ println!("cargo:rerun-if-changed=../../../nss/lib/ckfw/builtins/nssckbi.h");
+
+ let bindings = Builder::default()
+ .header("../../../nss/lib/ckfw/builtins/nssckbi.h")
+ .allowlist_var("NSS_BUILTINS_CRYPTOKI_VERSION_MAJOR")
+ .allowlist_var("NSS_BUILTINS_CRYPTOKI_VERSION_MINOR")
+ .allowlist_var("NSS_BUILTINS_LIBRARY_VERSION_MAJOR")
+ .allowlist_var("NSS_BUILTINS_LIBRARY_VERSION_MINOR")
+ .allowlist_var("NSS_BUILTINS_HARDWARE_VERSION_MAJOR")
+ .allowlist_var("NSS_BUILTINS_HARDWARE_VERSION_MINOR")
+ .allowlist_var("NSS_BUILTINS_FIRMWARE_VERSION_MAJOR")
+ .allowlist_var("NSS_BUILTINS_FIRMWARE_VERSION_MINOR")
+ .parse_callbacks(Box::new(PKCS11TypesParseCallbacks))
+ .generate()
+ .expect("Unable to generate bindings.");
+
+ let out_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set in env."));
+ bindings
+ .write_to_file(out_path.join("version.rs"))
+ .expect("Could not write version.rs.");
+
+ let mut out = BufWriter::new(
+ File::create(out_path.join("builtins.rs")).expect("Could not write builtins.rs."),
+ );
+
+ let input: &str = &std::fs::read_to_string("../../../nss/lib/ckfw/builtins/certdata.txt")
+ .expect("Unable to read certdata.txt.");
+
+ let blocks = match parse(input) {
+ Ok((_, blocks)) => blocks,
+ Err(e) => {
+ let input = match e {
+ nom::Err::Error(nom::error::Error { input, .. }) => input,
+ _ => "Unknown",
+ };
+ emit_build_error!(
+ out,
+ &format!(
+ "Could not parse certdata.txt. Failed at: \'{}\'\");",
+ input.escape_debug().to_string().escape_debug()
+ )
+ );
+ return Ok(());
+ }
+ };
+
+ let root_lists: Vec<&Block> = blocks
+ .iter()
+ .filter(|x| attr(x, "CKA_CLASS") == &Ck::Class("CKO_NSS_BUILTIN_ROOT_LIST"))
+ .collect();
+
+ if root_lists.len() != 1 {
+ emit_build_error!(
+ out,
+ "certdata.txt does not define a CKO_NSS_BUILTIN_ROOT_LIST object."
+ );
+ return Ok(());
+ }
+
+ let mut certs: Vec<&Block> = blocks
+ .iter()
+ .filter(|x| attr(x, "CKA_CLASS") == &Ck::Class("CKO_CERTIFICATE"))
+ .collect();
+
+ let trusts: Vec<&Block> = blocks
+ .iter()
+ .filter(|x| attr(x, "CKA_CLASS") == &Ck::Class("CKO_NSS_TRUST"))
+ .collect();
+
+ if certs.len() != trusts.len() {
+ emit_build_error!(
+ out,
+ "certdata.txt has a mismatched number of certificate and trust objects"
+ );
+ return Ok(());
+ }
+
+ // Ensure that every certificate has a CKA_SUBJECT attribute for the sort
+ for (i, cert) in certs.iter().enumerate() {
+ match cert.get("CKA_SUBJECT") {
+ Some(Ck::MultilineOctal(_)) => (),
+ _ => {
+ emit_build_error!(
+ out,
+ format!("Certificate {i} in certdata.txt has no CKA_SUBJECT attribute.")
+ );
+ return Ok(());
+ }
+ }
+ }
+
+ certs.sort_by_cached_key(|x| match x.get("CKA_SUBJECT") {
+ Some(Ck::MultilineOctal(data)) => octal_block_to_vec_u8(data),
+ _ => unreachable!(),
+ });
+
+ // Write out arrays for the DER encoded certificate, serial number, and subject of each root.
+ // Since the serial number and the subject are in the DER cert, we don't need to store
+ // additional data for them.
+ for (i, cert) in certs.iter().enumerate() {
+ // Preserve the comment from certdata.txt
+ match attr(cert, "COMMENT") {
+ Ck::Empty => (),
+ comment => write!(out, "{comment}")?,
+ };
+
+ let der = attr(cert, "CKA_VALUE");
+ writeln!(out, "static ROOT_{i}: &[u8] = {der};")?;
+
+ // Search for the serial number and subject in the DER cert. We want to search on the raw
+ // bytes, not the octal presentation, so we have to unpack the enums.
+ let der_data = match der {
+ Ck::MultilineOctal(x) => octal_block_to_vec_u8(x),
+ _ => unreachable!(),
+ };
+ let serial_data = match attr(cert, "CKA_SERIAL_NUMBER") {
+ Ck::MultilineOctal(x) => octal_block_to_vec_u8(x),
+ _ => unreachable!(),
+ };
+ let subject_data = match attr(cert, "CKA_SUBJECT") {
+ Ck::MultilineOctal(x) => octal_block_to_vec_u8(x),
+ _ => unreachable!(),
+ };
+
+ fn need_u16(out: &mut impl Write, attr: &str, what: &str, i: usize) -> std::io::Result<()> {
+ emit_build_error!(
+ out,
+ format!("Certificate {i} in certdata.txt has a {attr} whose {what} doesn't fit in a u8. Time to upgrade to u16 at the expense of size?")
+ );
+ Ok(())
+ }
+
+ let serial_len = serial_data.len();
+ if let Some(serial_offset) = &der_data.windows(serial_len).position(|s| s == serial_data) {
+ if *serial_offset > u8::MAX.into() {
+ return need_u16(&mut out, "CKA_SERIAL_NUMBER", "offset", i);
+ }
+ if serial_len > u8::MAX.into() {
+ return need_u16(&mut out, "CKA_SERIAL_NUMBER", "length", i);
+ }
+ writeln!(
+ out,
+ "const SERIAL_{i}: (u8, u8) = ({serial_offset}, {serial_len});"
+ )?;
+ } else {
+ emit_build_error!(
+ out,
+ format!("Certificate {i} in certdata.txt has a CKA_SERIAL_NUMBER that does not match its CKA_VALUE.")
+ );
+ return Ok(());
+ }
+
+ let subject_len = subject_data.len();
+ if let Some(subject_offset) = &der_data
+ .windows(subject_len)
+ .position(|s| s == subject_data)
+ {
+ if *subject_offset > u8::MAX.into() {
+ return need_u16(&mut out, "CKA_SUBJECT", "offset", i);
+ }
+ if subject_len > u8::MAX.into() {
+ return need_u16(&mut out, "CKA_SUBJECT", "length", i);
+ }
+ writeln!(
+ out,
+ "const SUBJECT_{i}: (u8, u8) = ({subject_offset}, {subject_len});"
+ )?;
+ } else {
+ emit_build_error!(
+ out,
+ format!("Certificate {i} in certdata.txt has a CKA_SUBJECT that does not match its CKA_VALUE.")
+ );
+ return Ok(());
+ }
+ }
+
+ let root_list_label = attr(root_lists[0], "CKA_LABEL");
+ let root_list_label_len = match root_list_label {
+ Ck::Utf8(x) => x.len() + 1,
+ _ => unreachable!(),
+ };
+ writeln!(
+ out,
+ "pub const ROOT_LIST_LABEL: [u8; {root_list_label_len}] = *b{root_list_label};"
+ )?;
+
+ writeln!(out, "pub static BUILTINS: [Root; {}] = [", certs.len())?;
+ for (i, cert) in certs.iter().enumerate() {
+ let subject = attr(cert, "CKA_SUBJECT");
+ let issuer = attr(cert, "CKA_ISSUER");
+ let label = attr(cert, "CKA_LABEL");
+ if !subject.eq(issuer) {
+ writeln!(out, "];")?; // end the definition of BUILTINS
+ let label = format!("{}", label);
+ writeln!(
+ out,
+ "std::compile_error!(\"Certificate with label {} is not self-signed\");",
+ label.escape_debug()
+ )?;
+ return Ok(());
+ }
+ let mozpol = attr(cert, "CKA_NSS_MOZILLA_CA_POLICY");
+ let server_distrust = attr(cert, "CKA_NSS_SERVER_DISTRUST_AFTER");
+ let email_distrust = attr(cert, "CKA_NSS_EMAIL_DISTRUST_AFTER");
+ let matching_trusts: Vec<&&Block> = trusts
+ .iter()
+ .filter(|trust| {
+ (attr(cert, "CKA_ISSUER") == attr(trust, "CKA_ISSUER"))
+ && (attr(cert, "CKA_SERIAL_NUMBER") == attr(trust, "CKA_SERIAL_NUMBER"))
+ })
+ .collect();
+ if matching_trusts.len() != 1 {
+ writeln!(out, "];")?; // end the definition of BUILTINS
+ let label = format!("{}", label);
+ writeln!(out, "std::compile_error!(\"Could not find unique trust object for {} in certdata.txt\");", label.escape_debug())?;
+ return Ok(());
+ }
+ let trust = *matching_trusts[0];
+ let sha1 = match attr(trust, "CKA_CERT_SHA1_HASH") {
+ Ck::MultilineOctal(x) => octal_block_to_hex_string(x),
+ _ => unreachable!(),
+ };
+ let md5 = match attr(trust, "CKA_CERT_MD5_HASH") {
+ Ck::MultilineOctal(x) => octal_block_to_hex_string(x),
+ _ => unreachable!(),
+ };
+ let server = attr(trust, "CKA_TRUST_SERVER_AUTH");
+ let email = attr(trust, "CKA_TRUST_EMAIL_PROTECTION");
+
+ writeln!(
+ out,
+ " Root {{
+ label: {label},
+ der_name: SUBJECT_{i},
+ der_serial: SERIAL_{i},
+ der_cert: ROOT_{i},
+ mozilla_ca_policy: {mozpol},
+ server_distrust_after: {server_distrust},
+ email_distrust_after: {email_distrust},
+ sha1: [{sha1}],
+ md5: [{md5}],
+ trust_server: {server},
+ trust_email: {email},
+ }},"
+ )?;
+ }
+ writeln!(out, "];")?;
+
+ let _ = out.flush();
+ Ok(())
+}