diff --git a/MODULE.bazel b/MODULE.bazel index c369b5438..afd0d61fd 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -87,6 +87,7 @@ register_toolchains( "@pigweed//pw_toolchain/arm_clang:arm_clang_cc_toolchain_cortex-m4", "@pigweed//pw_toolchain/riscv_clang:riscv_clang_cc_toolchain_rv32imc", "@pw_rust_toolchains//:all", + "@lowrisc_opentitan//third_party/rust:bindgen_toolchain", ) crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate") diff --git a/presubmit/license.py b/presubmit/license.py index 055d4fe4e..5599809af 100644 --- a/presubmit/license.py +++ b/presubmit/license.py @@ -48,6 +48,7 @@ # keep-sorted: end # Data files # keep-sorted: start + r"\.a$", r"\.bin$", r"\.csv$", r"\.der$", @@ -77,6 +78,7 @@ # keep-sorted: end # Generated files # keep-sorted: start + r"\btarget/earlgrey/cryptolib/src/otcrypto_sys\.rs$", r"\btarget/earlgrey/registers/.*", # keep-sorted: end # Generated third-party files diff --git a/target/earlgrey/cryptolib/BUILD.bazel b/target/earlgrey/cryptolib/BUILD.bazel new file mode 100644 index 000000000..8ccff4e10 --- /dev/null +++ b/target/earlgrey/cryptolib/BUILD.bazel @@ -0,0 +1,79 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust_bindgen//:defs.bzl", "rust_bindgen") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "otcrypto_headers", + hdrs = [ + "@lowrisc_opentitan//sw/device/lib/base/freestanding:assert.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:float.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:iso646.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:limits.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:stdalign.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:stdarg.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:stdbool.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:stddef.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:stdint.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:stdnoreturn.h", + "@lowrisc_opentitan//sw/device/lib/base/freestanding:string.h", + ], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "@lowrisc_opentitan//sw/device/lib/crypto/include:crypto_hdrs", + ], +) + +rust_bindgen( + name = "otcrypto_raw", + bindgen_flags = [ + "--use-core", + "--default-enum-style=rust", + "--allowlist-item=otcrypto_.*|hardened_bool.*|status_t.*|status", + "--no-layout-tests", + ], + cc_lib = ":otcrypto_headers", + clang_flags = [ + "-target", + "riscv32-unknown-none-elf", + "-nostdinc", + "-isystem", + "external/lowrisc_opentitan+/sw/device/lib/base/freestanding", + ], + header = "@lowrisc_opentitan//sw/device/lib/crypto/include:otcrypto.h", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +cc_import( + name = "libotcrypto", + static_library = "libotcrypto.a", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +genrule( + name = "otcrypto_sys_gen", + srcs = [":otcrypto_raw"], + outs = ["src/otcrypto_sys.rs"], + cmd = "$(location //tools/otcrypto_bindgen) --input $< --out $@", + tools = ["//tools/otcrypto_bindgen"], +) + +rust_library( + name = "cryptolib", + srcs = [ + "src/lib.rs", + ":otcrypto_sys_gen", + ], + crate_name = "earlgrey_cryptolib", + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + ":libotcrypto", + "@rust_crates//:zerocopy", + ], +) diff --git a/target/earlgrey/cryptolib/libotcrypto.a b/target/earlgrey/cryptolib/libotcrypto.a new file mode 100644 index 000000000..85ba321a2 Binary files /dev/null and b/target/earlgrey/cryptolib/libotcrypto.a differ diff --git a/target/earlgrey/cryptolib/src/lib.rs b/target/earlgrey/cryptolib/src/lib.rs new file mode 100644 index 000000000..5c0c46570 --- /dev/null +++ b/target/earlgrey/cryptolib/src/lib.rs @@ -0,0 +1,172 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Safe Rust wrapper and FFI bindings for OpenTitan `libotcrypto` on Earlgrey. + +#![no_std] + +pub mod otcrypto_sys; + +pub use otcrypto_sys::{ + hmac_hash_sha256, hmac_hash_sha384, hmac_hash_sha512, AesGcmTagLen, AesKeyMode, AesMode, + AesOperation, AesPadding, BlindedKey, ByteBuf, ConstByteBuf, ConstWord32Buf, HardenedBool, + HashDigest, HashMode, HmacKeyMode, KeyConfig, KeyMode, KeySecurityLevel, KeyType, LibVersion, + RsaPadding, RsaSize, Status, StatusValue, UnblindedKey, Word32Buf, +}; + +/// Error types for OpenTitan cryptolib operations. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CryptoError { + /// Invalid arguments passed to cryptolib. + BadArgs, + /// Internal hardware or driver error. + InternalError, + /// Fatal error, requires reset or re-initialization. + FatalError, + /// Asynchronous operation incomplete. + AsyncIncomplete, + /// Requested cryptographic primitive is not implemented. + NotImplemented, + /// Unknown status code returned by C library. + Unknown(i32), +} + +impl CryptoError { + /// Returns a string representation of the error. + pub const fn as_str(&self) -> &'static str { + match self { + CryptoError::BadArgs => "BadArgs", + CryptoError::InternalError => "InternalError", + CryptoError::FatalError => "FatalError", + CryptoError::AsyncIncomplete => "AsyncIncomplete", + CryptoError::NotImplemented => "NotImplemented", + CryptoError::Unknown(_) => "Unknown", + } + } +} + +/// Converts a Status returned by libotcrypto into a Rust Result. +pub fn status_to_result(status: Status) -> Result<(), CryptoError> { + if status.value == StatusValue::Ok as i32 { + Ok(()) + } else if status.value == StatusValue::BadArgs as i32 { + Err(CryptoError::BadArgs) + } else if status.value == StatusValue::InternalError as i32 { + Err(CryptoError::InternalError) + } else if status.value == StatusValue::FatalError as i32 { + Err(CryptoError::FatalError) + } else if status.value == StatusValue::AsyncIncomplete as i32 { + Err(CryptoError::AsyncIncomplete) + } else if status.value == StatusValue::NotImplemented as i32 { + Err(CryptoError::NotImplemented) + } else { + Err(CryptoError::Unknown(status.value)) + } +} + +pub const INIT_INTEGRITY_CHECKSUM: u32 = 0x5a3; + +#[inline] +fn compute_buf_checksum(data: *const u8, len: usize) -> u32 { + INIT_INTEGRITY_CHECKSUM + .wrapping_add(data as u32) + .wrapping_add(len as u32) +} + +impl ConstByteBuf { + /// Constructs a `ConstByteBuf` from an immutable byte slice. + pub fn from_slice(slice: &[u8]) -> Self { + let data = slice.as_ptr(); + let len = slice.len(); + Self { + data, + len, + ptr_checksum: compute_buf_checksum(data, len), + } + } +} + +impl ByteBuf { + /// Constructs a `ByteBuf` from a mutable byte slice. + pub fn from_mut_slice(slice: &mut [u8]) -> Self { + let data = slice.as_mut_ptr(); + let len = slice.len(); + Self { + data, + len, + ptr_checksum: compute_buf_checksum(data, len), + } + } +} + +impl ConstWord32Buf { + /// Constructs a `ConstWord32Buf` from an immutable 32-bit word slice. + pub fn from_words(words: &[u32]) -> Self { + let data = words.as_ptr(); + let len = words.len(); + Self { + data, + len, + ptr_checksum: compute_buf_checksum(data as *const u8, len), + } + } +} + +impl Word32Buf { + /// Constructs a `Word32Buf` from a mutable 32-bit word slice. + pub fn from_mut_words(words: &mut [u32]) -> Self { + let data = words.as_mut_ptr(); + let len = words.len(); + Self { + data, + len, + ptr_checksum: compute_buf_checksum(data as *const u8, len), + } + } +} + +impl BlindedKey { + /// Attaches key material buffer to the blinded key structure. + pub fn with_key_material(&mut self, km: &[u8]) -> &mut Self { + self.set_keyblob(km.as_ptr() as *mut u32); + self + } +} + +impl UnblindedKey { + /// Attaches key material buffer to the unblinded key structure. + pub fn with_key_material(&mut self, km: &[u8]) -> &mut Self { + self.set_key(km.as_ptr() as *mut u32); + self + } +} + +/// Initializes the cryptolib with the given security level. +pub fn init(security_level: KeySecurityLevel) -> Result<(), CryptoError> { + let status = unsafe { otcrypto_sys::init(security_level) }; + status_to_result(status) +} + +/// Computes SHA-256 digest over the input buffer in a single shot using Earlgrey HMAC HWIP. +pub fn sha256(data: &[u8], digest_out: &mut [u8; 32]) -> Result<(), CryptoError> { + let buf = ConstByteBuf::from_slice(data); + let status = + unsafe { otcrypto_sys::hmac_hash_sha256(&buf, digest_out.as_mut_ptr() as *mut u32) }; + status_to_result(status) +} + +/// Computes SHA-384 digest over the input buffer in a single shot using Earlgrey HMAC HWIP. +pub fn sha384(data: &[u8], digest_out: &mut [u8; 48]) -> Result<(), CryptoError> { + let buf = ConstByteBuf::from_slice(data); + let status = + unsafe { otcrypto_sys::hmac_hash_sha384(&buf, digest_out.as_mut_ptr() as *mut u32) }; + status_to_result(status) +} + +/// Computes SHA-512 digest over the input buffer in a single shot using Earlgrey HMAC HWIP. +pub fn sha512(data: &[u8], digest_out: &mut [u8; 64]) -> Result<(), CryptoError> { + let buf = ConstByteBuf::from_slice(data); + let status = + unsafe { otcrypto_sys::hmac_hash_sha512(&buf, digest_out.as_mut_ptr() as *mut u32) }; + status_to_result(status) +} diff --git a/target/earlgrey/tests/cryptolib_smoke/BUILD.bazel b/target/earlgrey/tests/cryptolib_smoke/BUILD.bazel new file mode 100644 index 000000000..029d44f5b --- /dev/null +++ b/target/earlgrey/tests/cryptolib_smoke/BUILD.bazel @@ -0,0 +1,114 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "test_cryptolib", + srcs = [ + "test_cryptolib.rs", + ], + codegen_crate_name = "test_cryptolib_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/cryptolib", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:zerocopy", + ], +) + +system_image( + name = "cryptolib_smoke", + apps = [ + ":test_cryptolib", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "cryptolib_smoke_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper310", + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":cryptolib_smoke", +) + +opentitan_test( + name = "cryptolib_smoke_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":cryptolib_smoke", +) + +opentitan_test( + name = "cryptolib_smoke_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":cryptolib_smoke", +) diff --git a/target/earlgrey/tests/cryptolib_smoke/system.json5 b/target/earlgrey/tests/cryptolib_smoke/system.json5 new file mode 100644 index 000000000..285d02c56 --- /dev/null +++ b/target/earlgrey/tests/cryptolib_smoke/system.json5 @@ -0,0 +1,55 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "test_cryptolib", + flash_size_bytes: 131072, + processes: [ + { + name: "test_cryptolib", + ram_size_bytes: 16384, + objects: [ + { + type: "thread", + name: "main_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "crypto_block", + type: "device", + start_address: 0x41100000, + size_bytes: 0x100000, + }, + { + name: "alert_handler", + type: "device", + start_address: 0x40150000, + size_bytes: 0x1000, + }, + { + name: "sensor_ctrl", + type: "device", + start_address: 0x40490000, + size_bytes: 0x1000, + }, + ], + }, + ], + }, + ], +} diff --git a/target/earlgrey/tests/cryptolib_smoke/target.rs b/target/earlgrey/tests/cryptolib_smoke/target.rs new file mode 100644 index 000000000..f1a80dc41 --- /dev/null +++ b/target/earlgrey/tests/cryptolib_smoke/target.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Cryptolib Smoke Test"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/cryptolib_smoke/test_cryptolib.rs b/target/earlgrey/tests/cryptolib_smoke/test_cryptolib.rs new file mode 100644 index 000000000..3ca133625 --- /dev/null +++ b/target/earlgrey/tests/cryptolib_smoke/test_cryptolib.rs @@ -0,0 +1,182 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use earlgrey_cryptolib::{ + sha256, sha384, sha512, BlindedKey, HardenedBool, KeyConfig, KeyMode, KeySecurityLevel, + LibVersion, UnblindedKey, +}; +use pw_status::Result; +use userspace::entry; +use zerocopy::IntoBytes; + +const TEST_INPUT: &[u8] = b"Hello, OpenPRoT!"; +const EXPECTED_SHA256_INPUT: [u8; 32] = [ + 0xb3, 0xf1, 0xc2, 0x5a, 0xb1, 0x8a, 0xea, 0x2d, 0x1c, 0x8c, 0xa2, 0x15, 0x69, 0x27, 0x92, 0xdf, + 0xc0, 0x41, 0x76, 0xcb, 0x35, 0x6c, 0xfb, 0x18, 0x5d, 0x8c, 0x23, 0xa7, 0xbc, 0x85, 0x3f, 0x24, +]; + +const EXPECTED_SHA256_EMPTY: [u8; 32] = [ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, +]; + +const EXPECTED_SHA384_EMPTY: [u8; 48] = [ + 0x38, 0xb0, 0x60, 0xa7, 0x51, 0xac, 0x96, 0x38, 0x4c, 0xd9, 0x32, 0x7e, 0xb1, 0xb1, 0xe3, 0x6a, + 0x21, 0xfd, 0xb7, 0x11, 0x14, 0xbe, 0x07, 0x43, 0x4c, 0x0c, 0xc7, 0xbf, 0x63, 0xf6, 0xe1, 0xda, + 0x27, 0x4e, 0xde, 0xbf, 0xe7, 0x6f, 0x65, 0xfb, 0xd5, 0x1a, 0xd2, 0xf1, 0x48, 0x98, 0xb9, 0x5b, +]; + +const EXPECTED_SHA512_EMPTY: [u8; 64] = [ + 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, 0x07, + 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, 0xe9, 0xce, + 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, + 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e, +]; + +fn test_cryptolib_smoke() -> Result<()> { + // Test 1: Safe SHA-256 on "Hello, OpenPRoT!" + pw_log::info!("Testing SHA-256 on \"Hello, OpenPRoT!\"..."); + let mut digest256 = [0u8; 32]; + match sha256(TEST_INPUT, &mut digest256) { + Ok(()) => { + if digest256 == EXPECTED_SHA256_INPUT { + pw_log::info!("SHA-256 test string matched."); + } else { + pw_log::error!("SHA-256 test string digest mismatch!"); + return Err(pw_status::Error::Internal.into()); + } + } + Err(e) => { + pw_log::error!("SHA-256 test string failed: {}", e.as_str()); + return Err(pw_status::Error::Internal.into()); + } + } + + // Test 2: Safe SHA-256 on empty string + pw_log::info!("Testing SHA-256 on empty input..."); + let mut empty_digest256 = [0u8; 32]; + match sha256(b"", &mut empty_digest256) { + Ok(()) => { + if empty_digest256 == EXPECTED_SHA256_EMPTY { + pw_log::info!("SHA-256 empty input matched."); + } else { + pw_log::error!("SHA-256 empty input digest mismatch!"); + return Err(pw_status::Error::Internal.into()); + } + } + Err(e) => { + pw_log::error!("SHA-256 empty input failed: {}", e.as_str()); + return Err(pw_status::Error::Internal.into()); + } + } + + // Test 3: Safe SHA-384 on empty string + pw_log::info!("Testing SHA-384 on empty input..."); + let mut empty_digest384 = [0u8; 48]; + match sha384(b"", &mut empty_digest384) { + Ok(()) => { + if empty_digest384 == EXPECTED_SHA384_EMPTY { + pw_log::info!("SHA-384 empty input matched."); + } else { + pw_log::error!("SHA-384 empty input digest mismatch!"); + return Err(pw_status::Error::Internal.into()); + } + } + Err(e) => { + pw_log::error!("SHA-384 empty input failed: {}", e.as_str()); + return Err(pw_status::Error::Internal.into()); + } + } + + // Test 4: Safe SHA-512 on empty string + pw_log::info!("Testing SHA-512 on empty input..."); + let mut empty_digest512 = [0u8; 64]; + match sha512(b"", &mut empty_digest512) { + Ok(()) => { + if empty_digest512 == EXPECTED_SHA512_EMPTY { + pw_log::info!("SHA-512 empty input matched."); + } else { + pw_log::error!("SHA-512 empty input digest mismatch!"); + return Err(pw_status::Error::Internal.into()); + } + } + Err(e) => { + pw_log::error!("SHA-512 empty input failed: {}", e.as_str()); + return Err(pw_status::Error::Internal.into()); + } + } + + // Test 5: Zerocopy BlindedKey and UnblindedKey serialization and pointer operations + pw_log::info!("Testing Zerocopy key types and pointer rewrites..."); + let mut blinded = BlindedKey { + config: KeyConfig { + version: LibVersion::V1, + key_mode: KeyMode::AesGcm, + key_length: 32, + hw_backed: HardenedBool::False, + exportable: HardenedBool::True, + security_level: KeySecurityLevel::Low, + }, + keyblob_length: 64, + keyblob: 0, + checksum: 0x12345678, + }; + + let mut key_buf = [0x55u32; 16]; + blinded.set_keyblob(key_buf.as_mut_ptr()); + if blinded.keyblob_ptr() != key_buf.as_mut_ptr() { + pw_log::error!("BlindedKey pointer conversion mismatch!"); + return Err(pw_status::Error::Internal.into()); + } + + // Verify zerocopy byte representation + let blinded_bytes = blinded.as_bytes(); + if blinded_bytes.len() != core::mem::size_of::() { + pw_log::error!("BlindedKey size mismatch in zerocopy serialization!"); + return Err(pw_status::Error::Internal.into()); + } + + let mut unblinded = UnblindedKey { + key_mode: KeyMode::AesGcm, + key_length: 32, + key: 0, + checksum: 0x87654321, + }; + unblinded.set_key(key_buf.as_mut_ptr()); + if unblinded.key_ptr() != key_buf.as_mut_ptr() { + pw_log::error!("UnblindedKey pointer conversion mismatch!"); + return Err(pw_status::Error::Internal.into()); + } + let unblinded_bytes = unblinded.as_bytes(); + if unblinded_bytes.len() != core::mem::size_of::() { + pw_log::error!("UnblindedKey size mismatch in zerocopy serialization!"); + return Err(pw_status::Error::Internal.into()); + } + + pw_log::info!("Zerocopy key types verified successfully."); + + Ok(()) +} + +#[entry] +fn entry() -> Result<()> { + pw_log::info!("🔄 RUNNING CRYPTOLIB SMOKE TEST"); + let ret = test_cryptolib_smoke(); + + if ret.is_err() { + pw_log::error!("❌ FAIL"); + } else { + pw_log::info!("✅ PASS"); + } + + ret +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in cryptolib smoke test"); + loop {} +} diff --git a/third_party/rules_rust_bindgen/bindgen_static_lib.patch b/third_party/rules_rust_bindgen/bindgen_static_lib.patch index 973297d7d..7e193e178 100644 --- a/third_party/rules_rust_bindgen/bindgen_static_lib.patch +++ b/third_party/rules_rust_bindgen/bindgen_static_lib.patch @@ -24,3 +24,8 @@ diff -ur a/private/bindgen.bzl b/private/bindgen.bzl - args.add("--rust-edition=%s" % rust_toolchain.default_edition) + edition = rust_toolchain.default_edition or "2024" + args.add("--rust-edition=%s" % edition) +@@ -287,3 +291,3 @@ + resource_dir = _get_resource_dir(cc_toolchain) +- if resource_dir: ++ if resource_dir and not any([f.startswith("-resource-dir") or f == "-no-resource-dir" for f in ctx.attr.clang_flags]): + args.add("-resource-dir=%s" % resource_dir) diff --git a/tools/otcrypto_bindgen/BUILD.bazel b/tools/otcrypto_bindgen/BUILD.bazel new file mode 100644 index 000000000..47a87c0eb --- /dev/null +++ b/tools/otcrypto_bindgen/BUILD.bazel @@ -0,0 +1,22 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_binary( + name = "otcrypto_bindgen", + srcs = ["src/main.rs"], + edition = "2024", + deps = [ + "@rust_crates//:clap", + "@rust_crates//:quote", + "@rust_crates//:syn", + ], +) + +rust_test( + name = "otcrypto_bindgen_test", + crate = ":otcrypto_bindgen", +) diff --git a/tools/otcrypto_bindgen/src/main.rs b/tools/otcrypto_bindgen/src/main.rs new file mode 100644 index 000000000..a06403ef2 --- /dev/null +++ b/tools/otcrypto_bindgen/src/main.rs @@ -0,0 +1,492 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use quote::quote; +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; +use syn::{parse_quote, Attribute, Fields, File, Ident, Item, ItemStruct, Type}; + +#[derive(Parser, Debug)] +#[command( + author, + version, + about = "Transform Rust FFI bindings for OpenTitan libotcrypto using Rust AST (syn)" +)] +struct Args { + /// Path to raw input Rust bindings file (generated by bindgen) + #[arg(short, long)] + input: PathBuf, + + /// Output path for generated idiomatic Rust bindings + #[arg( + short, + long, + default_value = "target/earlgrey/cryptolib/src/otcrypto_sys.rs" + )] + out: PathBuf, +} + +pub fn to_pascal_case(s: &str) -> String { + let mut result = String::new(); + let mut capitalize_next = true; + for c in s.chars() { + if c == '_' { + capitalize_next = true; + } else if capitalize_next { + result.push(c.to_ascii_uppercase()); + capitalize_next = false; + } else { + result.push(c); + } + } + result +} + +pub fn sanitize_variant(name: &str) -> String { + if let Some(first) = name.chars().next() { + if first.is_ascii_digit() { + format!("V{name}") + } else { + name.to_string() + } + } else { + name.to_string() + } +} + +pub fn rename_type(name: &str) -> Option { + const PRIMITIVES: &[&str] = &[ + "u8", "u16", "u32", "u64", "usize", "i8", "i16", "i32", "i64", "isize", "bool", "c_void", + ]; + if PRIMITIVES.contains(&name) { + return None; + } + let clean = name.strip_suffix("_t").unwrap_or(name); + let stripped = clean.strip_prefix("otcrypto_").unwrap_or(clean); + let pascal = to_pascal_case(stripped); + if pascal == name { + None + } else { + Some(pascal) + } +} + +pub fn map_enum_variant_name(enum_name: &str, variant_name: &str) -> String { + match variant_name { + name if name.starts_with("kHardenedBool") => { + let stripped = name.strip_prefix("kHardenedBool").unwrap_or(name); + to_pascal_case(stripped) + } + name if name.starts_with("kOtcrypto") => { + let mut stripped = name.strip_prefix("kOtcrypto").unwrap_or(name); + let clean = enum_name.strip_prefix("enum ").unwrap_or(enum_name); + let clean = clean.strip_prefix("otcrypto_").unwrap_or(clean); + let clean = clean.strip_suffix("_t").unwrap_or(clean); + + let pascal_ename = to_pascal_case(clean); + if let Some(tail) = stripped.strip_prefix(&pascal_ename) + && !tail.is_empty() + { + stripped = tail; + } else if let Some(tail) = stripped.strip_prefix("HashXofMode") + && !tail.is_empty() + { + stripped = tail; + } + + let pascal = to_pascal_case(stripped); + sanitize_variant(&pascal) + } + _ => sanitize_variant(&to_pascal_case(variant_name)), + } +} + +pub fn rename_fn(name: &str) -> Option { + name.strip_prefix("otcrypto_").map(ToString::to_string) +} + +/// Check whether a type contains raw pointers. +fn has_raw_pointer(ty: &Type) -> bool { + match ty { + Type::Ptr(_) => true, + Type::Array(a) => has_raw_pointer(&a.elem), + Type::Slice(s) => has_raw_pointer(&s.elem), + Type::Reference(r) => has_raw_pointer(&r.elem), + Type::Tuple(t) => t.elems.iter().any(has_raw_pointer), + _ => false, + } +} + +/// Automatically determines if a struct is a Plain-Old-Data (POD) struct +/// that can safely derive zerocopy traits (has no pointer fields). +pub fn is_pod_struct(s: &ItemStruct) -> bool { + match &s.fields { + Fields::Named(fields) => !fields.named.iter().any(|f| has_raw_pointer(&f.ty)), + Fields::Unnamed(fields) => !fields.unnamed.iter().any(|f| has_raw_pointer(&f.ty)), + Fields::Unit => true, + } +} + +fn add_zerocopy_derives(attrs: &mut Vec) { + for attr in attrs.iter_mut() { + if attr.path().is_ident("derive") + && let syn::Meta::List(ref mut list) = attr.meta + { + let tokens_str = list.tokens.to_string(); + if !tokens_str.contains("IntoBytes") { + let old_tokens = &list.tokens; + list.tokens = parse_quote!(#old_tokens, IntoBytes, Immutable, KnownLayout); + } + return; + } + } + attrs.push(parse_quote!(#[derive(IntoBytes, Immutable, KnownLayout)])); +} + +fn rename_type_in_type(ty: &mut Type) { + match ty { + Type::Path(tp) => { + if let Some(last) = tp.path.segments.last_mut() { + let name = last.ident.to_string(); + if let Some(new_name) = rename_type(&name) { + last.ident = Ident::new(&new_name, last.ident.span()); + } + } + } + Type::Ptr(p) => { + rename_type_in_type(&mut p.elem); + } + Type::Reference(r) => { + rename_type_in_type(&mut r.elem); + } + Type::Array(a) => { + rename_type_in_type(&mut a.elem); + } + Type::Slice(s) => { + rename_type_in_type(&mut s.elem); + } + _ => {} + } +} + +pub fn transform_ast(source: &str) -> Result> { + let mut file: File = syn::parse_file(source)?; + + // 1. Process structs, enums, functions, and types + for item in &mut file.items { + match item { + Item::Struct(s) => { + let orig_name = s.ident.to_string(); + if let Some(new_name) = rename_type(&orig_name) { + s.ident = Ident::new(&new_name, s.ident.span()); + } + let current_name = s.ident.to_string(); + + if let Fields::Named(ref mut fields) = s.fields { + for field in &mut fields.named { + let fname = field + .ident + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_default(); + if current_name.ends_with("Key") && (fname == "keyblob" || fname == "key") { + field.ty = parse_quote!(u32); + } else { + rename_type_in_type(&mut field.ty); + } + } + } + + if is_pod_struct(s) { + add_zerocopy_derives(&mut s.attrs); + } + } + Item::Enum(e) => { + let orig_name = e.ident.to_string(); + if let Some(new_name) = rename_type(&orig_name) { + e.ident = Ident::new(&new_name, e.ident.span()); + } + + for variant in &mut e.variants { + let vname = variant.ident.to_string(); + let clean_vname = map_enum_variant_name(&orig_name, &vname); + variant.ident = Ident::new(&clean_vname, variant.ident.span()); + } + + add_zerocopy_derives(&mut e.attrs); + } + Item::ForeignMod(fm) => { + for foreign_item in &mut fm.items { + if let syn::ForeignItem::Fn(f) = foreign_item { + let orig_name = f.sig.ident.to_string(); + if let Some(new_name) = rename_fn(&orig_name) { + f.sig.ident = Ident::new(&new_name, f.sig.ident.span()); + } + for arg in &mut f.sig.inputs { + if let syn::FnArg::Typed(pat_type) = arg { + rename_type_in_type(&mut pat_type.ty); + } + } + if let syn::ReturnType::Type(_, ref mut ret_ty) = f.sig.output { + rename_type_in_type(ret_ty); + } + } + } + } + Item::Type(t) => { + let orig_name = t.ident.to_string(); + if let Some(new_name) = rename_type(&orig_name) { + t.ident = Ident::new(&new_name, t.ident.span()); + } + rename_type_in_type(&mut t.ty); + } + _ => {} + } + } + + // 2. Filter out redundant type aliases and use items + let defined_structs_and_enums: HashSet = file + .items + .iter() + .filter_map(|item| match item { + Item::Struct(s) => Some(s.ident.to_string()), + Item::Enum(e) => Some(e.ident.to_string()), + _ => None, + }) + .collect(); + + file.items.retain(|item| match item { + Item::Use(_) => false, + Item::Const(c) => c.ident != "_", + Item::Type(t) => { + let alias_name = t.ident.to_string(); + let target_name = match &*t.ty { + Type::Path(tp) => tp + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_default(), + _ => String::new(), + }; + alias_name != target_name && !defined_structs_and_enums.contains(&alias_name) + } + _ => true, + }); + + // 3. Inject zerocopy imports and header comments + let mut output = String::from( + "// Generated by otcrypto_bindgen (AST pipeline). DO NOT EDIT.\n\ + #![allow(non_upper_case_globals, non_camel_case_types, non_snake_case, dead_code)]\n\n\ + use zerocopy::{Immutable, IntoBytes, KnownLayout};\n\n", + ); + + let code_tokens = quote!(#file); + output.push_str(&code_tokens.to_string()); + + // 4. Append accessor helper methods and hardware driver bindings + let helpers = r#" + +impl BlindedKey { + /// Sets the pointer integer value to the keyblob buffer. + #[inline] + pub fn set_keyblob(&mut self, ptr: *mut u32) { + self.keyblob = ptr as u32; + } + + /// Returns the raw pointer to the keyblob buffer. + #[inline] + pub fn keyblob_ptr(&self) -> *mut u32 { + self.keyblob as *mut u32 + } +} + +impl UnblindedKey { + /// Sets the pointer integer value to the key buffer. + #[inline] + pub fn set_key(&mut self, ptr: *mut u32) { + self.key = ptr as u32; + } + + /// Returns the raw pointer to the key buffer. + #[inline] + pub fn key_ptr(&self) -> *mut u32 { + self.key as *mut u32 + } +} + +unsafe extern "C" { + /// Direct hardware-accelerated SHA-256 computation via Earlgrey HMAC HWIP. + pub fn hmac_hash_sha256(msg: *const ConstByteBuf, digest: *mut u32) -> Status; + + /// Direct hardware-accelerated SHA-384 computation via Earlgrey HMAC HWIP. + pub fn hmac_hash_sha384(msg: *const ConstByteBuf, digest: *mut u32) -> Status; + + /// Direct hardware-accelerated SHA-512 computation via Earlgrey HMAC HWIP. + pub fn hmac_hash_sha512(msg: *const ConstByteBuf, digest: *mut u32) -> Status; +} +"#; + output.push_str(helpers); + + Ok(output) +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + let raw_rust = fs::read_to_string(&args.input) + .map_err(|e| format!("Failed to read raw input file {:?}: {e}", args.input))?; + + let transformed = transform_ast(&raw_rust)?; + + if let Some(parent) = args.out.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&args.out, transformed)?; + println!( + "Successfully wrote idiomatic Rust FFI bindings to {:?}", + args.out + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_pascal_case() { + assert_eq!(to_pascal_case("key_config"), "KeyConfig"); + assert_eq!(to_pascal_case("blinded_key"), "BlindedKey"); + assert_eq!(to_pascal_case("status_value"), "StatusValue"); + } + + #[test] + fn test_sanitize_variant() { + assert_eq!(sanitize_variant("Aes"), "Aes"); + assert_eq!(sanitize_variant("128"), "V128"); + assert_eq!(sanitize_variant("Sha256"), "Sha256"); + } + + #[test] + fn test_rename_type() { + assert_eq!(rename_type("otcrypto_status_t"), Some("Status".to_string())); + assert_eq!(rename_type("status_t"), Some("Status".to_string())); + assert_eq!( + rename_type("hardened_bool_t"), + Some("HardenedBool".to_string()) + ); + assert_eq!( + rename_type("otcrypto_status_value_t"), + Some("StatusValue".to_string()) + ); + assert_eq!( + rename_type("otcrypto_blinded_key_t"), + Some("BlindedKey".to_string()) + ); + assert_eq!( + rename_type("otcrypto_key_config_t"), + Some("KeyConfig".to_string()) + ); + } + + #[test] + fn test_rename_fn() { + assert_eq!(rename_fn("otcrypto_sha2_256"), Some("sha2_256".to_string())); + assert_eq!( + rename_fn("otcrypto_aes_gcm_encrypt"), + Some("aes_gcm_encrypt".to_string()) + ); + } + + #[test] + fn test_enum_variant_mappings() { + assert_eq!( + map_enum_variant_name("hardened_bool_t", "kHardenedBoolTrue"), + "True".to_string() + ); + assert_eq!( + map_enum_variant_name("otcrypto_status_value_t", "kOtcryptoStatusValueOk"), + "Ok".to_string() + ); + assert_eq!( + map_enum_variant_name("otcrypto_key_type_t", "kOtcryptoKeyTypeAes"), + "Aes".to_string() + ); + assert_eq!( + map_enum_variant_name("otcrypto_rsa_size_t", "kOtcryptoRsaSize2048"), + "V2048".to_string() + ); + assert_eq!( + map_enum_variant_name("otcrypto_hash_mode_t", "kOtcryptoHashModeSha3_256"), + "Sha3256".to_string() + ); + assert_eq!( + map_enum_variant_name("otcrypto_hash_mode_t", "kOtcryptoHashXofModeShake128"), + "Shake128".to_string() + ); + } + + #[test] + fn test_transform_ast_end_to_end() { + let raw_snippet = r#" +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct otcrypto_blinded_key { + pub config: otcrypto_key_config_t, + pub keyblob_length: usize, + pub keyblob: *mut u32, + pub checksum: u32, +} +pub type otcrypto_blinded_key_t = otcrypto_blinded_key; + +#[repr(u32)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum otcrypto_key_type { + kOtcryptoKeyTypeAes = 0, + kOtcryptoKeyTypeHmac = 1, +} +pub type otcrypto_key_type_t = otcrypto_key_type; + +unsafe extern "C" { + pub fn otcrypto_sha2_256( + message: otcrypto_const_byte_buf_t, + digest: *mut otcrypto_hash_digest_t, + ) -> status_t; +} +"#; + let result = transform_ast(raw_snippet).expect("AST transform failed"); + assert!(result.contains("struct BlindedKey")); + assert!(result.contains("keyblob : u32") || result.contains("keyblob: u32")); + assert!(result.contains("enum KeyType")); + assert!(result.contains("Aes = 0")); + assert!(result.contains("Hmac = 1")); + assert!(result.contains("fn sha2_256")); + assert!(result.contains("IntoBytes")); + assert!(result.contains("Immutable")); + assert!(result.contains("KnownLayout")); + } + + #[test] + fn test_is_pod_struct() { + let pod: ItemStruct = parse_quote! { + pub struct Foo { + pub a: u32, + pub b: usize, + pub c: [u32; 16], + } + }; + assert!(is_pod_struct(&pod)); + + let with_ptr: ItemStruct = parse_quote! { + pub struct Bar { + pub data: *mut u8, + pub len: usize, + } + }; + assert!(!is_pod_struct(&with_ptr)); + } +}