embedded-mbedtls-sys 0.2.0

no_std sys-crate for Mbed TLS
Documentation
// Copyright Open Logistics Foundation
//
// Licensed under the Open Logistics Foundation License 1.3.
// For details on the licensing terms, see the LICENSE file.
// SPDX-License-Identifier: OLFL-1.3

use std::path::PathBuf;
use std::{fmt::Write as _, fs::File, io::Write};

/// Build configuration, contains paths and config items which will be used in several build steps
struct Config {
    /// Path to Mbed TLS source repo
    mbedtls_src: PathBuf,
    /// Path to the Mbed TLS include directory
    mbedtls_include: PathBuf,
    /// Path to config.h file which will be generated for this build
    config_h: PathBuf,
    /// Path to bindings.rs which contains the bindgen-generated Rust bindings
    bindings_rs: PathBuf,
    /// Additional C flags which will be passed to CMake and the bindgen invocation
    cflags: Vec<String>,
}

fn main() {
    #[cfg(feature = "flexi_logger")]
    flexi_logger::Logger::try_with_env_or_str("trace")
        .unwrap()
        .start()
        .unwrap();

    let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR not set"));
    let mbedtls_src = PathBuf::from("3rdparty/mbedtls");
    let mbedtls_include = mbedtls_src.join("include");
    let config_h = out_dir.join("config.h");
    let bindings_rs = out_dir.join("bindings.rs");
    let cflags: Vec<String> = vec![];

    let config = Config {
        mbedtls_src,
        mbedtls_include,
        config_h,
        bindings_rs,
        cflags,
    };

    write_config_h(&config);
    print_rerun_files(&config);
    run_cmake(&config);
    run_bindgen(&config);
}

/// Generates and writes the config.h file which will be used for the Mbed TLS build
fn write_config_h(build_config: &crate::Config) {
    let file = &build_config.config_h;
    let mut f = File::create(file).expect("Could not create config.h file '{file}'");

    // Helper closure to `#define` macros for the config.h file
    let mut define = |item| {
        writeln!(f, "#define {item}").expect("Could not write to config.h file");
    };

    // Config file inspired by the config-ccm-psk-dtls1_2.h file from the mbedtls release

    // For the TLS_PSK_WITH_AES_128_CCM_8 suite:
    // AES
    define("MBEDTLS_AES_C");
    // CCM
    define("MBEDTLS_CCM_C");
    // SHA256
    define("MBEDTLS_MD_C");
    define("MBEDTLS_SHA256_C");
    // PSK
    define("MBEDTLS_KEY_EXCHANGE_PSK_ENABLED");
    define("MBEDTLS_PSK_MAX_LEN 16");

    // Enable TLS with DTLS support
    define("MBEDTLS_SSL_TLS_C");
    define("MBEDTLS_CIPHER_C");
    define("MBEDTLS_SSL_PROTO_TLS1_2");
    define("MBEDTLS_SSL_PROTO_DTLS");
    define("MBEDTLS_SSL_COOKIE_C");
    define("MBEDTLS_SSL_DTLS_ANTI_REPLAY");

    #[cfg(not(any(feature = "MBEDTLS_SSL_CLI_C", feature = "MBEDTLS_SSL_SRV_C")))]
    compile_error!("At least one feature of 'MBEDTLS_SSL_CLI_C' (TLS client) or 'MBEDTLS_SSL_SRV_C' (TLS server) should be selected");
    // Client
    #[cfg(feature = "MBEDTLS_SSL_CLI_C")]
    define("MBEDTLS_SSL_CLI_C");
    // Server
    #[cfg(feature = "MBEDTLS_SSL_SRV_C")]
    define("MBEDTLS_SSL_SRV_C");

    // Cryptographic Pseudo Random Number Generator (PRNG). Still, a good entropy source is required to
    // seed/initialize the PRNG.
    // Alternatively to this one, MBEDTLS_HMAC_DRBG_C could be used.
    define("MBEDTLS_CTR_DRBG_C");

    // Enables support for RFC 6066 max_fragment_length extension in SSL.
    define("MBEDTLS_SSL_MAX_FRAGMENT_LENGTH");

    // Error messages and TLS debugging traces
    // (huge code size increase, e.g. around 30kB in release mode for thumbv7em-none-eabihf)
    //
    // TODO Enable via Rust feature
    //define("MBEDTLS_DEBUG_C");
    define("MBEDTLS_ERROR_C");
}

/// Print some `cargo:rerun-if-changed` instructions to ensure rebuilds when required
fn print_rerun_files(conf: &Config) {
    // Our build depends on the vendored mbedtls source. Since cargo is smart enough to scan a full
    // directory
    // (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed), it is
    // enough to specify this single
    println!("cargo:rerun-if-changed={}", conf.mbedtls_src.display());
}

/// Call CMake to build the Mbed TLS library
fn run_cmake(conf: &Config) {
    let mut cmk = cmake::Config::new(&conf.mbedtls_src);
    // Set the config.h file path
    cmk.cflag(format!(
        r#"-DMBEDTLS_CONFIG_FILE="\"{}\"""#,
        conf.config_h.to_str().expect("config.h UTF-8 error")
    ));
    // Disabling examples, tests and file generation avoids a Python and/or Perl dependency.
    // Enabling any of these does not seem to offer any benefit for usage from Rust.
    cmk.define("ENABLE_PROGRAMS", "OFF")
        .define("ENABLE_TESTING", "OFF")
        .define("GEN_FILES", "OFF");
    cmk.build_target("install");
    // Set all additional cflags
    for cflag in &conf.cflags {
        cmk.cflag(cflag);
    }

    // Configuration required for bare-metal arm targets
    let target = std::env::var("TARGET")
        .expect("TARGET environment variable should be set in build scripts");
    // thumbv6m-none-eabi, thumbv7em-none-eabi, thumbv7em-none-eabihf, thumbv7m-none-eabi
    // probably use arm-none-eabi-gcc which can cause the cmake compiler test to fail.
    if target.starts_with("thumbv") && target.contains("none-eabi") {
        // When building on Linux, -rdynamic flag is added automatically. Changing the
        // CMAKE_SYSTEM_NAME to Generic avoids this.
        cmk.define("CMAKE_SYSTEM_NAME", "Generic");
        // The compiler test requires _exit which is not available. By just trying to compile
        // a library, we can fix it.
        cmk.define("CMAKE_TRY_COMPILE_TARGET_TYPE", "STATIC_LIBRARY");
    }

    let dst = cmk.build();

    // cmake installs the headers to OUT_DIR/include and the static libraries to OUT_DIR/lib.
    // For some 64bit systems, static libraries are placed under lib64. This is achieved by using
    // the `GNUInstallDirs` module, see https://stackoverflow.com/a/76528304.
    // Apparently, it is not easily possible to query the resulting installation directory from the
    // cmake process. So for simplicity, we just add both "lib" and "lib64" as search paths.
    let add_library_search_path = |dir| {
        let mut dst = dst.clone();
        dst.push(dir);
        let library_dir = dst.to_str().expect("link-search UTF-8 error");
        println!("cargo:rustc-link-search=native={library_dir}");
    };
    add_library_search_path("lib");
    add_library_search_path("lib64");

    println!("cargo:rustc-link-lib=mbedtls");
    println!("cargo:rustc-link-lib=mbedx509");
    println!("cargo:rustc-link-lib=mbedcrypto");
}

/// Run bindgen to generate the corresponding Rust bindings and write them to the bindings.rs file
fn run_bindgen(conf: &Config) {
    // List of header files which should be included in the bindgen generation step
    let header_files = [
        // main mbedtls tls connection interface
        "mbedtls/ssl.h",
        // counter-mode deterministic random bit genenrator, one of two PRNGs shipped with mbedtls,
        // requires MBEDTLS_CTR_DRBG_C
        "mbedtls/ctr_drbg.h",
        // gives access to the mbedtls_debug_set_threshold() function which must be called to
        // enable debug output, requires MBEDTLS_DEBUG_C
        "mbedtls/debug.h",
        // gives access to the mbedtls_strerror() function to translate mbedtls error codes into a
        // string representation, requires MBEDTLS_ERROR_C
        "mbedtls/error.h",
    ];

    // Write the header files to a temporary String
    let mut header = String::new();
    for header_file in header_files {
        writeln!(header, "#include <{header_file}>").unwrap();
    }

    // Get the currently configured C compiler to pass its flags as clang_args to bindgen
    let mut cc = cc::Build::new();
    // Additionally specified cflags
    for cflag in &conf.cflags {
        cc.flag(cflag);
    }
    // Include path
    cc.include(&conf.mbedtls_include);
    // mbedtls_config.h file path
    let config_file_path = conf.config_h.to_str().expect("config.h UTF-8 error");
    cc.define(
        "MBEDTLS_CONFIG_FILE",
        format!(r#""{}""#, config_file_path).as_str(),
    );

    // Determine the sysroot for this compiler so that bindgen uses the correct headers
    let compiler = cc.get_compiler();
    if compiler.is_like_gnu() {
        let output = compiler.to_command().args(["--print-sysroot"]).output();
        match output {
            Ok(sysroot) if sysroot.status.success() => {
                let path = std::str::from_utf8(&sysroot.stdout).expect("Malformed sysroot");
                let trimmed_path = path
                    .strip_suffix("\r\n")
                    .or(path.strip_suffix('\n'))
                    .unwrap_or(path);
                cc.flag(&format!("--sysroot={}", trimmed_path));
            }
            _ => {} // Do nothing for toolchains without a configured sysroot
        };
    }

    // Generate bindings
    //
    // Since all public functions/types are properly prefixed with "mbedtls_" or "psa_", we can use
    // these prefixes as regexes for the allowlists. Additionally, we set the right set of
    // clang_args according to the current build configuration. These especially contain the
    // sysroot for (bare-metal) cross-compilation and the MBEDTLS_CONFIG_FILE define.
    let bindings = bindgen::builder()
        .enable_function_attribute_detection()
        .clang_args(
            cc.get_compiler()
                .args()
                .iter()
                .map(|arg| arg.to_str().unwrap()),
        )
        .header_contents("bindgen-input.h", &header)
        .allowlist_recursively(false)
        .use_core()
        .ctypes_prefix("core::ffi")
        .default_enum_style(bindgen::EnumVariation::Consts)
        .generate_comments(false)
        .derive_copy(true)
        .derive_debug(true)
        .derive_default(true)
        .prepend_enum_name(false)
        .translate_enum_integer_types(true)
        .layout_tests(false)
        .allowlist_function("^(?i)mbedtls_.*")
        .allowlist_type("^(?i)mbedtls_.*")
        .allowlist_var("^(?i)mbedtls_.*")
        .allowlist_function("^(?i)psa_.*")
        .allowlist_type("^(?i)psa_.*")
        .allowlist_var("^(?i)psa_.*")
        .generate()
        .expect("bindgen failed")
        .to_string();

    // Write generated bindings to bindings.rs which will be included in lib.rs
    File::create(&conf.bindings_rs)
        .expect("Could not create/open bindings.rs")
        .write_all(bindings.as_bytes())
        .expect("Could not write bindgen bindings to bindings.rs");
}