diff --git a/uefi-test-runner/https/README.md b/uefi-test-runner/https/README.md new file mode 100644 index 000000000..caf9517f9 --- /dev/null +++ b/uefi-test-runner/https/README.md @@ -0,0 +1,7 @@ +https ca cert database in efi signature list format + +Copied over from centos stream 9 where this is available as +/etc/pki/ca-trust/extracted/edk2/cacerts.bin + +It's the Mozilla Foundation CA certificate list, shipped in +ca-certificates.rpm, licensed as "MIT AND GPL-2.0-or-later". diff --git a/uefi-test-runner/https/cacerts.bin b/uefi-test-runner/https/cacerts.bin new file mode 100644 index 000000000..6a9a48ad3 Binary files /dev/null and b/uefi-test-runner/https/cacerts.bin differ diff --git a/uefi-test-runner/src/proto/network/http.rs b/uefi-test-runner/src/proto/network/http.rs new file mode 100644 index 000000000..225dbfeef --- /dev/null +++ b/uefi-test-runner/src/proto/network/http.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use alloc::vec::Vec; + +use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; +use uefi::proto::device_path::DevicePath; +use uefi::proto::network::http::{HttpBinding, HttpHelper}; +use uefi::proto::network::ip4config2::Ip4Config2; +use uefi::{boot, Handle}; + +use uefi_raw::protocol::network::http::HttpStatusCode; + +pub fn print_handle_devpath(prefix: &str, handle: &Handle) { + let Ok(dp) = boot::open_protocol_exclusive::(*handle) else { + info!("{}no device path for handle", prefix); + return; + }; + if let Ok(string) = dp.to_string(DisplayOnly(true), AllowShortcuts(true)) { + info!("{}{}", prefix, string); + } +} + +fn fetch_http(handle: Handle, url: &str) -> Option> { + info!("http: fetching {} ...", url); + + let http_res = HttpHelper::new(handle); + if let Err(e) = http_res { + error!("http new: {}", e); + return None; + } + let mut http = http_res.unwrap(); + + let res = http.configure(); + if let Err(e) = res { + error!("http configure: {}", e); + return None; + } + + let res = http.request_get(url); + if let Err(e) = res { + error!("http request: {}", e); + return None; + } + + let res = http.response_first(true); + if let Err(e) = res { + error!("http response: {}", e); + return None; + } + + let rsp = res.unwrap(); + if rsp.status != HttpStatusCode::STATUS_200_OK { + error!("http server error: {:?}", rsp.status); + return None; + } + let Some(cl_hdr) = rsp.headers.iter().find(|h| h.0 == "content-length") else { + // The only way to figure when your transfer is complete is to + // get the content length header and count the bytes you got. + // So missing header -> fatal error. + error!("no content length"); + return None; + }; + let Ok(cl) = cl_hdr.1.parse::() else { + error!("parse content length ({})", cl_hdr.1); + return None; + }; + info!("http: size is {} bytes", cl); + + let mut data = rsp.body; + loop { + if data.len() >= cl { + break; + } + + let res = http.response_more(); + if let Err(e) = res { + error!("read response: {}", e); + return None; + } + + let mut buf = res.unwrap(); + data.append(&mut buf); + } + + Some(data) +} + +pub fn test() { + info!("Testing ip4 config2 + http protocols"); + + let handles = boot::locate_handle_buffer(boot::SearchType::from_proto::()) + .expect("get nic handles"); + + for h in handles.as_ref() { + print_handle_devpath("nic: ", h); + + info!("Bring up interface (ip4 config2 protocol)"); + let mut ip4 = Ip4Config2::new(*h).expect("open ip4 config2 protocol"); + ip4.ifup(true).expect("acquire ipv4 address"); + + // hard to find web sites which still allow plain http these days ... + info!("Testing HTTP"); + fetch_http(*h, "http://example.com/").expect("http request failed"); + + // FYI: not all firmware builds support modern tls versions. + // request() -> ABORTED typically is a tls handshake error. + // check the firmware log for details. + info!("Testing HTTPS"); + fetch_http( + *h, + "https://raw.githubusercontent.com/rust-osdev/uefi-rs/refs/heads/main/Cargo.toml", + ) + .expect("https request failed"); + + info!("PASSED"); + } +} diff --git a/uefi-test-runner/src/proto/network/mod.rs b/uefi-test-runner/src/proto/network/mod.rs index 1789c3783..0e21db626 100644 --- a/uefi-test-runner/src/proto/network/mod.rs +++ b/uefi-test-runner/src/proto/network/mod.rs @@ -3,9 +3,11 @@ pub fn test() { info!("Testing Network protocols"); + http::test(); pxe::test(); snp::test(); } +mod http; mod pxe; mod snp; diff --git a/uefi/src/proto/network/http.rs b/uefi/src/proto/network/http.rs new file mode 100644 index 000000000..c808476e2 --- /dev/null +++ b/uefi/src/proto/network/http.rs @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![cfg(feature = "alloc")] + +//! HTTP Protocol. +//! +//! See [`Http`]. + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::ffi::{c_char, c_void, CStr}; +use core::ptr; +use log::debug; + +use uefi::boot::ScopedProtocol; +use uefi::prelude::*; +use uefi::proto::unsafe_protocol; +use uefi_raw::protocol::driver::ServiceBindingProtocol; +use uefi_raw::protocol::network::http::{ + HttpAccessPoint, HttpConfigData, HttpHeader, HttpMessage, HttpMethod, HttpProtocol, + HttpRequestData, HttpResponseData, HttpStatusCode, HttpToken, HttpV4AccessPoint, HttpVersion, +}; + +/// HTTP [`Protocol`]. Send HTTP Requests. +/// +/// [`Protocol`]: uefi::proto::Protocol +#[derive(Debug)] +#[unsafe_protocol(HttpProtocol::GUID)] +pub struct Http(HttpProtocol); + +impl Http { + /// Receive HTTP Protocol configuration. + pub fn get_mode_data(&mut self, config_data: &mut HttpConfigData) -> uefi::Result<()> { + let status = unsafe { (self.0.get_mode_data)(&mut self.0, config_data) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } + + /// Configure HTTP Protocol. Must be called before sending HTTP requests. + pub fn configure(&mut self, config_data: &HttpConfigData) -> uefi::Result<()> { + let status = unsafe { (self.0.configure)(&mut self.0, config_data) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } + + /// Send HTTP request. + pub fn request(&mut self, token: &mut HttpToken) -> uefi::Result<()> { + let status = unsafe { (self.0.request)(&mut self.0, token) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } + + /// Cancel HTTP request. + pub fn cancel(&mut self, token: &mut HttpToken) -> uefi::Result<()> { + let status = unsafe { (self.0.cancel)(&mut self.0, token) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } + + /// Receive HTTP response. + pub fn response(&mut self, token: &mut HttpToken) -> uefi::Result<()> { + let status = unsafe { (self.0.response)(&mut self.0, token) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } + + /// Poll network stack for updates. + pub fn poll(&mut self) -> uefi::Result<()> { + let status = unsafe { (self.0.poll)(&mut self.0) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } +} + +/// HTTP Service Binding Protocol. +#[derive(Debug)] +#[unsafe_protocol(HttpProtocol::SERVICE_BINDING_GUID)] +pub struct HttpBinding(ServiceBindingProtocol); + +impl HttpBinding { + /// Create HTTP Protocol Handle. + pub fn create_child(&mut self) -> uefi::Result { + let mut c_handle = ptr::null_mut(); + let status; + let handle; + unsafe { + status = (self.0.create_child)(&mut self.0, &mut c_handle); + handle = Handle::from_ptr(c_handle); + }; + match status { + Status::SUCCESS => Ok(handle.unwrap()), + _ => Err(status.into()), + } + } + + /// Destroy HTTP Protocol Handle. + pub fn destroy_child(&mut self, handle: Handle) -> uefi::Result<()> { + let status = unsafe { (self.0.destroy_child)(&mut self.0, handle.as_ptr()) }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } +} + +/// HTTP Response data +#[derive(Debug)] +pub struct HttpHelperResponse { + /// HTTP Status + pub status: HttpStatusCode, + /// HTTP Response Headers + pub headers: Vec<(String, String)>, + /// HTTP Body + pub body: Vec, +} + +/// HTTP Helper, makes using the HTTP protocol more convenient. +#[derive(Debug)] +pub struct HttpHelper { + child_handle: Handle, + binding: ScopedProtocol, + protocol: Option>, +} + +impl HttpHelper { + /// Create new HTTP helper instance for the given NIC handle. + pub fn new(nic_handle: Handle) -> uefi::Result { + let mut binding = unsafe { + boot::open_protocol::( + boot::OpenProtocolParams { + handle: nic_handle, + agent: boot::image_handle(), + controller: None, + }, + boot::OpenProtocolAttributes::GetProtocol, + )? + }; + debug!("http: binding proto ok"); + + let child_handle = binding.create_child()?; + debug!("http: child handle ok"); + + let protocol_res = unsafe { + boot::open_protocol::( + boot::OpenProtocolParams { + handle: child_handle, + agent: boot::image_handle(), + controller: None, + }, + boot::OpenProtocolAttributes::GetProtocol, + ) + }; + if let Err(e) = protocol_res { + let _ = binding.destroy_child(child_handle); + return Err(e); + } + debug!("http: protocol ok"); + + Ok(Self { + child_handle, + binding, + protocol: Some(protocol_res.unwrap()), + }) + } + + /// Configure the HTTP Protocol with some sane defaults. + pub fn configure(&mut self) -> uefi::Result<()> { + let ip4 = HttpV4AccessPoint { + use_default_addr: true.into(), + ..Default::default() + }; + + let config = HttpConfigData { + http_version: HttpVersion::HTTP_VERSION_10, + time_out_millisec: 10_000, + local_addr_is_ipv6: false.into(), + access_point: HttpAccessPoint { ipv4_node: &ip4 }, + }; + + self.protocol.as_mut().unwrap().configure(&config)?; + debug!("http: configure ok"); + + Ok(()) + } + + /// Send HTTP request + pub fn request( + &mut self, + method: HttpMethod, + url: &str, + body: Option<&mut [u8]>, + ) -> uefi::Result<()> { + let url16 = uefi::CString16::try_from(url).unwrap(); + + let Some(hostname) = url.split('/').nth(2) else { + return Err(Status::INVALID_PARAMETER.into()); + }; + let mut c_hostname = String::from(hostname); + c_hostname.push('\0'); + debug!("http: host: {}", hostname); + + let mut tx_req = HttpRequestData { + method, + url: url16.as_ptr().cast::(), + }; + + let mut tx_hdr = Vec::new(); + tx_hdr.push(HttpHeader { + field_name: c"Host".as_ptr().cast::(), + field_value: c_hostname.as_ptr(), + }); + + let mut tx_msg = HttpMessage::default(); + tx_msg.data.request = &mut tx_req; + tx_msg.header_count = tx_hdr.len(); + tx_msg.header = tx_hdr.as_mut_ptr(); + if body.is_some() { + let b = body.unwrap(); + tx_msg.body_length = b.len(); + tx_msg.body = b.as_mut_ptr().cast::(); + } + + let mut tx_token = HttpToken { + status: Status::NOT_READY, + message: &mut tx_msg, + ..Default::default() + }; + + let p = self.protocol.as_mut().unwrap(); + p.request(&mut tx_token)?; + debug!("http: request sent ok"); + + loop { + if tx_token.status != Status::NOT_READY { + break; + } + p.poll()?; + } + + if tx_token.status != Status::SUCCESS { + return Err(tx_token.status.into()); + }; + + debug!("http: request status ok"); + + Ok(()) + } + + /// Send HTTP GET request + pub fn request_get(&mut self, url: &str) -> uefi::Result<()> { + self.request(HttpMethod::GET, url, None)?; + Ok(()) + } + + /// Send HTTP HEAD request + pub fn request_head(&mut self, url: &str) -> uefi::Result<()> { + self.request(HttpMethod::HEAD, url, None)?; + Ok(()) + } + + /// Receive the start of the http response, the headers and (parts of) the body. + pub fn response_first(&mut self, expect_body: bool) -> uefi::Result { + let mut rx_rsp = HttpResponseData { + status_code: HttpStatusCode::STATUS_UNSUPPORTED, + }; + + let mut body = vec![0; if expect_body { 16 * 1024 } else { 0 }]; + let mut rx_msg = HttpMessage::default(); + rx_msg.data.response = &mut rx_rsp; + rx_msg.body_length = body.len(); + rx_msg.body = if !body.is_empty() { + body.as_mut_ptr() + } else { + ptr::null() + } as *mut c_void; + + let mut rx_token = HttpToken { + status: Status::NOT_READY, + message: &mut rx_msg, + ..Default::default() + }; + + let p = self.protocol.as_mut().unwrap(); + p.response(&mut rx_token)?; + + loop { + if rx_token.status != Status::NOT_READY { + break; + } + p.poll()?; + } + + debug!( + "http: response: {} / {:?}", + rx_token.status, rx_rsp.status_code + ); + + if rx_token.status != Status::SUCCESS && rx_token.status != Status::HTTP_ERROR { + return Err(rx_token.status.into()); + }; + + debug!("http: headers: {}", rx_msg.header_count); + let mut headers: Vec<(String, String)> = Vec::new(); + for i in 0..rx_msg.header_count { + let n; + let v; + unsafe { + n = CStr::from_ptr((*rx_msg.header.add(i)).field_name.cast::()); + v = CStr::from_ptr((*rx_msg.header.add(i)).field_value.cast::()); + } + headers.push(( + n.to_str().unwrap().to_lowercase(), + String::from(v.to_str().unwrap()), + )); + } + + debug!("http: body: {}/{}", rx_msg.body_length, body.len()); + + let rsp = HttpHelperResponse { + status: rx_rsp.status_code, + headers, + body: body[0..rx_msg.body_length].to_vec(), + }; + Ok(rsp) + } + + /// Receive more body data. + pub fn response_more(&mut self) -> uefi::Result> { + let mut body = vec![0; 16 * 1024]; + let mut rx_msg = HttpMessage { + body_length: body.len(), + body: body.as_mut_ptr().cast::(), + ..Default::default() + }; + + let mut rx_token = HttpToken { + status: Status::NOT_READY, + message: &mut rx_msg, + ..Default::default() + }; + + let p = self.protocol.as_mut().unwrap(); + p.response(&mut rx_token)?; + + loop { + if rx_token.status != Status::NOT_READY { + break; + } + p.poll()?; + } + + debug!("http: response: {}", rx_token.status); + + if rx_token.status != Status::SUCCESS { + return Err(rx_token.status.into()); + }; + + debug!("http: body: {}/{}", rx_msg.body_length, body.len()); + + Ok(body[0..rx_msg.body_length].to_vec()) + } +} + +impl Drop for HttpHelper { + fn drop(&mut self) { + // protocol must go out of scope before calling destroy_child + self.protocol = None; + let _ = self.binding.destroy_child(self.child_handle); + } +} diff --git a/uefi/src/proto/network/ip4config2.rs b/uefi/src/proto/network/ip4config2.rs new file mode 100644 index 000000000..013c659d4 --- /dev/null +++ b/uefi/src/proto/network/ip4config2.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +#![cfg(feature = "alloc")] + +//! IP4 Config2 Protocol. + +use alloc::vec; +use alloc::vec::Vec; +use core::ffi::c_void; + +use uefi::boot::ScopedProtocol; +use uefi::prelude::*; +use uefi::proto::unsafe_protocol; +use uefi::{print, println}; +use uefi_raw::protocol::network::ip4_config2::{ + Ip4Config2DataType, Ip4Config2InterfaceInfo, Ip4Config2Policy, Ip4Config2Protocol, +}; +use uefi_raw::Ipv4Address; + +/// IP4 Config2 [`Protocol`]. Configure IPv4 networking. +/// +/// [`Protocol`]: uefi::proto::Protocol +#[derive(Debug)] +#[unsafe_protocol(Ip4Config2Protocol::GUID)] +pub struct Ip4Config2(pub Ip4Config2Protocol); + +impl Ip4Config2 { + /// Open IP4 Config2 protocol for the given NIC handle. + pub fn new(nic_handle: Handle) -> uefi::Result> { + let protocol; + unsafe { + protocol = boot::open_protocol::( + boot::OpenProtocolParams { + handle: nic_handle, + agent: boot::image_handle(), + controller: None, + }, + boot::OpenProtocolAttributes::GetProtocol, + )?; + } + Ok(protocol) + } + + /// Set configuration data. It is recommended to type-specific set_* helpers instead of calling this directly. + pub fn set_data(&mut self, data_type: Ip4Config2DataType, data: &mut [u8]) -> uefi::Result<()> { + let status = unsafe { + let data_ptr = data.as_mut_ptr().cast::(); + (self.0.set_data)(&mut self.0, data_type, data.len(), data_ptr) + }; + match status { + Status::SUCCESS => Ok(()), + _ => Err(status.into()), + } + } + + /// Get configuration data. It is recommended to type-specific get_* helpers instead of calling this directly. + pub fn get_data(&mut self, data_type: Ip4Config2DataType) -> uefi::Result> { + let mut data_size = 0; + + // call #1: figure return buffer size + let status = unsafe { + let null = core::ptr::null_mut(); + (self.0.get_data)(&mut self.0, data_type, &mut data_size, null) + }; + if status != Status::BUFFER_TOO_SMALL { + return Err(status.into()); + } + + // call #2: get data + let mut data = vec![0; data_size]; + let status = unsafe { + let data_ptr = data.as_mut_ptr().cast::(); + (self.0.get_data)(&mut self.0, data_type, &mut data_size, data_ptr) + }; + match status { + Status::SUCCESS => Ok(data), + _ => Err(status.into()), + } + } + + /// Set config policy (static vs. dhcp). + pub fn set_policy(&mut self, policy: Ip4Config2Policy) -> uefi::Result<()> { + let mut data: [u8; 4] = policy.0.to_ne_bytes(); + self.set_data(Ip4Config2DataType::POLICY, &mut data) + } + + /// Get current interface configuration. + pub fn get_interface_info(&mut self) -> uefi::Result { + let data = self.get_data(Ip4Config2DataType::INTERFACE_INFO)?; + let info: &Ip4Config2InterfaceInfo = + unsafe { &*(data.as_ptr().cast::()) }; + Ok(Ip4Config2InterfaceInfo { + name: info.name, + if_type: info.if_type, + hw_addr_size: info.hw_addr_size, + hw_addr: info.hw_addr, + station_addr: info.station_addr, + subnet_mask: info.subnet_mask, + route_table_size: 0, + route_table: core::ptr::null_mut(), + }) + } + + fn print_info(info: &Ip4Config2InterfaceInfo) { + println!( + "addr v4: {}.{}.{}.{}", + info.station_addr.0[0], + info.station_addr.0[1], + info.station_addr.0[2], + info.station_addr.0[3], + ); + } + + /// Bring up network interface. Does nothing in case the network + /// is already set up. Otherwise turns on DHCP and waits until an + /// IPv4 address has been assigned. Reports progress on the + /// console if verbose is set to true. Returns TIMEOUT error in + /// case DHCP configuration does not finish within 30 seconds. + pub fn ifup(&mut self, verbose: bool) -> uefi::Result<()> { + let no_address = Ipv4Address::default(); + + let info = self.get_interface_info()?; + if info.station_addr != no_address { + if verbose { + print!("Network is already up: "); + Self::print_info(&info); + } + return Ok(()); + } + + if verbose { + print!("DHCP "); + } + self.set_policy(Ip4Config2Policy::DHCP)?; + + for _ in 0..30 { + if verbose { + print!("."); + } + boot::stall(1_000_000); + let info = self.get_interface_info()?; + if info.station_addr != no_address { + if verbose { + print!(" OK: "); + Self::print_info(&info); + } + return Ok(()); + } + } + + Err(Status::TIMEOUT.into()) + } +} diff --git a/uefi/src/proto/network/mod.rs b/uefi/src/proto/network/mod.rs index 3be7885e0..b3eb665b6 100644 --- a/uefi/src/proto/network/mod.rs +++ b/uefi/src/proto/network/mod.rs @@ -4,6 +4,8 @@ //! //! These protocols can be used to interact with network resources. +pub mod http; +pub mod ip4config2; pub mod pxe; pub mod snp; diff --git a/xtask/src/qemu.rs b/xtask/src/qemu.rs index a7e22c934..90c2081ca 100644 --- a/xtask/src/qemu.rs +++ b/xtask/src/qemu.rs @@ -359,6 +359,8 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> { if arch == UefiArch::IA32 || arch == UefiArch::X86_64 { cmd.args(["-debugcon", "file:./integration-test-debugcon.log"]); + cmd.args(["-chardev", "file,id=fw,path=./ovmf-firmware-debugcon.log"]); + cmd.args(["-device", "isa-debugcon,chardev=fw,iobase=0x402"]); } // Set the boot menu timeout to zero. On aarch64 in particular this speeds @@ -529,6 +531,12 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> { None }; + // Pass CA certificate database to the edk2 firmware, for TLS support. + cmd.args([ + "-fw_cfg", + "name=etc/edk2/https/cacerts,file=uefi-test-runner/https/cacerts.bin", + ]); + // Set up a software TPM if requested. let _tpm = if let Some(tpm_version) = opt.tpm { let tpm = Swtpm::spawn(tpm_version)?;