diff --git a/c-wrapper/src/lib.rs b/c-wrapper/src/lib.rs index a5cc3bd..1ea4c3a 100644 --- a/c-wrapper/src/lib.rs +++ b/c-wrapper/src/lib.rs @@ -294,15 +294,15 @@ struct ShimVerificationApi { /// It is the caller's responsibility to ensure that `nonce` is: /// /// - **EITHER** a null pointer (in which case the nonce challenge is issued on -/// the server side) +/// the server side) /// - **OR** a pointer to a valid buffer of initialized data of at -/// least `nonce_size` bytes, which will not be mutated for the duration -/// of this function call. +/// least `nonce_size` bytes, which will not be mutated for the duration +/// of this function call. /// /// It is the caller's responsibility to ensure that `out_session` is /// not a null pointer. #[no_mangle] -pub unsafe extern "C" fn open_challenge_response_session( +pub async unsafe extern "C" fn open_challenge_response_session( new_session_url: *const libc::c_char, nonce_size: libc::size_t, nonce: *const u8, @@ -349,7 +349,7 @@ pub unsafe extern "C" fn open_challenge_response_session( // This now won't panic because we dealt with errors by early return above. let cr = cr.unwrap(); - let newsession = cr.new_session(&nonce_converted); + let newsession = cr.new_session(&nonce_converted).await; match newsession { Ok(_) => {} @@ -428,15 +428,15 @@ pub unsafe extern "C" fn open_challenge_response_session( /// The caller guarantees the following: /// /// - The `session` parameter is a non-NULL pointer to a valid structure that was received from a prior -/// successful call to [`open_challenge_response_session()`]. Do not call this function with a NULL -/// pointer or a pointer to uninitialized data. Also do not call this function with a pointer to a -/// failed session. +/// successful call to [`open_challenge_response_session()`]. Do not call this function with a NULL +/// pointer or a pointer to uninitialized data. Also do not call this function with a pointer to a +/// failed session. /// - The `evidence` parameter is not NULL, and points to a valid data buffer of at least `evidence_size` -/// bytes that will not be mutated for the duration of this function call. +/// bytes that will not be mutated for the duration of this function call. /// - The `media_type` parameter is a non-NULL pointer to a valid NUL-terminated character string that -/// will not be mutated for the duration of this function call. +/// will not be mutated for the duration of this function call. #[no_mangle] -pub unsafe extern "C" fn challenge_response( +pub async unsafe extern "C" fn challenge_response( session: *mut ChallengeResponseSession, evidence_size: libc::size_t, evidence: *const u8, @@ -459,11 +459,15 @@ pub unsafe extern "C" fn challenge_response( // Actually call the client let client_result = match shim_session.client.as_ref() { - Some(client) => client.challenge_response( - evidence_bytes, - media_type_str, - shim_session.session_url_cstring.to_str().unwrap(), - ), + Some(client) => { + client + .challenge_response( + evidence_bytes, + media_type_str, + shim_session.session_url_cstring.to_str().unwrap(), + ) + .await + } // If we have no client, it means that the session was never properly established in the first place. None => Err(Error::ConfigError( "Cannot supply evidence because there is no session endpoint.".to_string(), @@ -531,7 +535,7 @@ pub unsafe extern "C" fn free_challenge_response_session(session: *mut Challenge /// It is the caller's responsibility to ensure that `out_api` is /// not a null pointer. #[no_mangle] -pub unsafe extern "C" fn veraison_get_verification_api( +pub async unsafe extern "C" fn veraison_get_verification_api( veraison_service_base_url: *const libc::c_char, out_api: *mut *mut VeraisonVerificationApi, ) -> VeraisonResult { @@ -541,7 +545,7 @@ pub unsafe extern "C" fn veraison_get_verification_api( url_cstr.to_str().unwrap() }; - let api = safe_get_verification_api(url_str); + let api = safe_get_verification_api(url_str).await; if let Err(e) = api { return stub_verification_api_from_error(&e, out_api); @@ -580,10 +584,10 @@ pub unsafe extern "C" fn veraison_free_verification_api( // veraison_get_verification_api. // This returns proper Rust errors, meanings that it can be coded using Rust-style error handling, // which helps with the several potential fail points in this flow. -fn safe_get_verification_api(base_url: &str) -> Result { +async fn safe_get_verification_api(base_url: &str) -> Result { let discovery = Discovery::from_base_url(String::from(base_url))?; - let verification_api = discovery.get_verification_api()?; + let verification_api = discovery.get_verification_api().await?; let public_key_der_vec = verification_api.ear_verification_key_as_der()?; @@ -835,6 +839,8 @@ mod tests { let result = unsafe { veraison_get_verification_api(base_url.as_ptr(), &mut verification_api) }; + let result = result.await; + // We should have an Ok result assert_eq!(result, VeraisonResult::Ok); @@ -916,6 +922,8 @@ mod tests { ) }; + let result = result.await; + // We should have an Ok result assert_eq!(result, VeraisonResult::Ok); @@ -954,6 +962,8 @@ mod tests { ) }; + let result = result.await; + // We should have an Ok result assert_eq!(result, VeraisonResult::Ok); diff --git a/rust-client/Cargo.toml b/rust-client/Cargo.toml index 50968fe..2b8b3e4 100644 --- a/rust-client/Cargo.toml +++ b/rust-client/Cargo.toml @@ -26,4 +26,4 @@ features = ["base64", "chrono"] [dev-dependencies] wiremock = "0.5" -async-std = { version = "1.6.5", features = ["attributes"] } +async-std = { version = "1.6.5", features = ["attributes", "tokio1"] } diff --git a/rust-client/examples/challenge_response.rs b/rust-client/examples/challenge_response.rs index e5e089e..e078136 100644 --- a/rust-client/examples/challenge_response.rs +++ b/rust-client/examples/challenge_response.rs @@ -1,30 +1,44 @@ -// Copyright 2022 Contributors to the Veraison project. +// Copyright 2022-2024 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 extern crate veraison_apiclient; use veraison_apiclient::*; -fn my_evidence_builder(nonce: &[u8], accept: &[String]) -> Result<(Vec, String), Error> { +fn my_evidence_builder( + nonce: &[u8], + accept: &[String], + token: Vec, +) -> Result<(Vec, String), Error> { println!("server challenge: {:?}", nonce); println!("acceptable media types: {:#?}", accept); - - Ok(( + let mut token = token; + if token.is_empty() { // some very fake evidence - vec![0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + token = vec![0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + } + Ok(( + token, // the first acceptable evidence type accept[0].to_string(), )) } -fn main() { - let base_url = "http://127.0.0.1:8080"; +#[async_std::main] +async fn main() { + let base_url = "https://localhost:8080"; + + let discovery_api_endpoint = format!("{}{}", base_url, "/.well-known/veraison/verification"); - let discovery = Discovery::from_base_url(String::from(base_url)) + let discovery = DiscoveryBuilder::new() + .with_url(discovery_api_endpoint) + .with_root_certificate("veraison-root.crt".into()) + .build() .expect("Failed to start API discovery with the service."); let verification_api = discovery .get_verification_api() + .await .expect("Failed to discover the verification endpoint details."); let relative_endpoint = verification_api @@ -36,13 +50,14 @@ fn main() { // create a ChallengeResponse object let cr = ChallengeResponseBuilder::new() .with_new_session_url(api_endpoint) + .with_root_certificate("veraison-root.crt".into()) .build() .unwrap(); let nonce = Nonce::Value(vec![0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef]); // alternatively, to let Veraison pick the challenge: "let nonce = Nonce::Size(32);" - match cr.run(nonce, my_evidence_builder) { + match cr.run(nonce, my_evidence_builder, Vec::new()).await { Err(e) => println!("Error: {}", e), Ok(attestation_result) => println!("Attestation Result: {}", attestation_result), } diff --git a/rust-client/examples/veraison-root.crt b/rust-client/examples/veraison-root.crt new file mode 100644 index 0000000..e894c3d --- /dev/null +++ b/rust-client/examples/veraison-root.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBfDCCASGgAwIBAgIUGFllXaV04uJz42tPnHXwOkaux50wCgYIKoZIzj0EAwIw +EzERMA8GA1UECgwIVmVyYWlzb24wHhcNMjQwNTIxMTAxNzA1WhcNMzQwNTE5MTAx +NzA1WjATMREwDwYDVQQKDAhWZXJhaXNvbjBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABCYxQeR0gnM4/4CvQBmIgNSm6SAal29OYm7GBpq/y0rZWolA3FlHChm3nIZe +qXAtKvK4rkolWSLiaRNN1mEWYG6jUzBRMB0GA1UdDgQWBBTq/aQhL7+hx9EOG+X0 +Q/YbAWuGDjAfBgNVHSMEGDAWgBTq/aQhL7+hx9EOG+X0Q/YbAWuGDjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQCAqRST0CFtgWVXpBtYoTldREXb +hGryGCivO3Jkv6LZ5wIhAMqlRBGBPbz8sgS+QQCA0pbhXFt7kMQpH3hrR/tEIeW2 +-----END CERTIFICATE----- diff --git a/rust-client/src/lib.rs b/rust-client/src/lib.rs index 401c718..87ca841 100644 --- a/rust-client/src/lib.rs +++ b/rust-client/src/lib.rs @@ -1,8 +1,16 @@ -// Copyright 2022 Contributors to the Veraison project. +// Copyright 2022-2024 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 #![allow(clippy::multiple_crate_versions)] +use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, +}; + +use reqwest::{Certificate, ClientBuilder}; + #[derive(thiserror::Error, PartialEq, Eq)] pub enum Error { #[error("configuration error: {0}")] @@ -25,6 +33,12 @@ impl From for Error { } } +impl From for Error { + fn from(re: std::io::Error) -> Self { + Error::ConfigError(re.to_string()) + } +} + impl From for Error { fn from(e: jsonwebkey::ConversionError) -> Self { Error::DataConversionError(e.to_string()) @@ -49,12 +63,13 @@ impl std::fmt::Debug for Error { /// The application is passed the session nonce and the list of supported /// evidence media types and shall return the computed evidence together with /// the selected media type. -type EvidenceCreationCb = fn(nonce: &[u8], accepted: &[String]) -> Result<(Vec, String), Error>; +type EvidenceCreationCb = + fn(nonce: &[u8], accepted: &[String], token: Vec) -> Result<(Vec, String), Error>; /// A builder for ChallengeResponse objects pub struct ChallengeResponseBuilder { new_session_url: Option, - // TODO(tho) add TLS config / authn tokens etc. + root_certificate: Option, } impl ChallengeResponseBuilder { @@ -62,6 +77,7 @@ impl ChallengeResponseBuilder { pub fn new() -> Self { Self { new_session_url: None, + root_certificate: None, } } @@ -73,16 +89,34 @@ impl ChallengeResponseBuilder { self } + pub fn with_root_certificate(mut self, v: PathBuf) -> ChallengeResponseBuilder { + self.root_certificate = Some(v); + self + } + /// Instantiate a valid ChallengeResponse object, or fail with an error. pub fn build(self) -> Result { let new_session_url_str = self .new_session_url .ok_or_else(|| Error::ConfigError("missing API endpoint".to_string()))?; + let mut http_client_builder: ClientBuilder = reqwest::ClientBuilder::new(); + + http_client_builder = http_client_builder.use_rustls_tls(); + + if self.root_certificate.is_some() { + let mut buf = Vec::new(); + File::open(self.root_certificate.unwrap())?.read_to_end(&mut buf)?; + let cert = Certificate::from_pem(&buf)?; + http_client_builder = http_client_builder.add_root_certificate(cert); + } + + let http_client = http_client_builder.build()?; + Ok(ChallengeResponse { new_session_url: url::Url::parse(&new_session_url_str) .map_err(|e| Error::ConfigError(e.to_string()))?, - http_client: reqwest::blocking::Client::builder().build()?, + http_client, }) } } @@ -97,7 +131,7 @@ impl Default for ChallengeResponseBuilder { /// be run. Always use the [ChallengeResponseBuilder] to instantiate it. pub struct ChallengeResponse { new_session_url: url::Url, - http_client: reqwest::blocking::Client, + http_client: reqwest::Client, } /// Nonce configuration: either the size (Size) of the nonce generated by the @@ -112,19 +146,23 @@ impl ChallengeResponse { /// Run a challenge-response verification session using the supplied nonce /// configuration and evidence creation callback. Returns the raw attestation results, or an /// error on failure. - pub fn run( + pub async fn run( &self, nonce: Nonce, evidence_creation_cb: EvidenceCreationCb, + token: Vec, ) -> Result { // create new c/r verification session on the veraison side - let (session_url, session) = self.new_session(&nonce)?; + let (session_url, session) = self.new_session(&nonce).await?; // invoke the user-provided evidence builder callback with per-session parameters - let (evidence, media_type) = (evidence_creation_cb)(session.nonce(), session.accept())?; + let (evidence, media_type) = + (evidence_creation_cb)(session.nonce(), session.accept(), token)?; // send evidence for verification to the session endpoint - let attestation_result = self.challenge_response(&evidence, &media_type, &session_url)?; + let attestation_result = self + .challenge_response(&evidence, &media_type, &session_url) + .await?; // return veraison's attestation results Ok(attestation_result) @@ -133,9 +171,12 @@ impl ChallengeResponse { /// Ask Veraison to create a new challenge/response session using the supplied nonce /// configuration. On success, the return value is a tuple of the session URL for subsequent /// operations, plus the session data including the nonce and the list of accept types. - pub fn new_session(&self, nonce: &Nonce) -> Result<(String, ChallengeResponseSession), Error> { + pub async fn new_session( + &self, + nonce: &Nonce, + ) -> Result<(String, ChallengeResponseSession), Error> { // ask veraison for a new session object - let resp = self.new_session_request(nonce)?; + let resp = self.new_session_request(nonce).await?; // expect 201 and a Location header containing the URI of the newly // allocated session @@ -149,7 +190,7 @@ impl ChallengeResponse { // middleware that is unaware of the API. We need something // more robust here that dispatches based on the Content-Type // header. - let pd: ProblemDetails = resp.json()?; + let pd: ProblemDetails = resp.json().await?; return Err(Error::ApiError(format!( "newSession response has unexpected status: {}. Details: {}", @@ -175,13 +216,13 @@ impl ChallengeResponse { .map_err(|e| Error::ApiError(e.to_string()))?; // decode returned session object - let crs: ChallengeResponseSession = resp.json()?; + let crs: ChallengeResponseSession = resp.json().await?; Ok((session_url.to_string(), crs)) } /// Execute a challenge/response operation with the given evidence. - pub fn challenge_response( + pub async fn challenge_response( &self, evidence: &[u8], media_type: &str, @@ -194,14 +235,15 @@ impl ChallengeResponse { .header(reqwest::header::ACCEPT, CRS_MEDIA_TYPE) .header(reqwest::header::CONTENT_TYPE, media_type) .body(evidence.to_owned()) - .send()?; + .send() + .await?; let status = resp.status(); if status.is_success() { match status { reqwest::StatusCode::OK => { - let crs: ChallengeResponseSession = resp.json()?; + let crs: ChallengeResponseSession = resp.json().await?; if crs.status != "complete" { return Err(Error::ApiError(format!( @@ -228,7 +270,7 @@ impl ChallengeResponse { ))), } } else { - let pd: ProblemDetails = resp.json()?; + let pd: ProblemDetails = resp.json().await?; Err(Error::ApiError(format!( "session response has error status: {}. Details: {}", @@ -237,14 +279,16 @@ impl ChallengeResponse { } } - fn new_session_request(&self, nonce: &Nonce) -> Result { + async fn new_session_request(&self, nonce: &Nonce) -> Result { let u = self.new_session_request_url(nonce)?; let r = self .http_client .post(u.as_str()) .header(reqwest::header::ACCEPT, CRS_MEDIA_TYPE) - .send()?; + .send() + .await + .unwrap(); Ok(r) } @@ -403,15 +447,75 @@ impl VerificationApi { } } +/// A builder for VerificationApi objects +pub struct DiscoveryBuilder { + url: Option, + root_certificate: Option, +} + +impl DiscoveryBuilder { + /// default constructor + pub fn new() -> Self { + Self { + url: None, + root_certificate: None, + } + } + + /// Use this method to supply the URL of the discovery endpoint + pub fn with_url(mut self, v: String) -> DiscoveryBuilder { + self.url = Some(v); + self + } + + pub fn with_root_certificate(mut self, v: PathBuf) -> DiscoveryBuilder { + self.root_certificate = Some(v); + self + } + + /// Instantiate a valid Discovery object, or fail with an error. + pub fn build(self) -> Result { + let url = self + .url + .ok_or_else(|| Error::ConfigError("missing API endpoint".to_string()))?; + + let mut http_client_builder: ClientBuilder = reqwest::ClientBuilder::new(); + + http_client_builder = http_client_builder.use_rustls_tls(); + + if self.root_certificate.is_some() { + let mut buf = Vec::new(); + File::open(self.root_certificate.unwrap())?.read_to_end(&mut buf)?; + let cert = Certificate::from_pem(&buf)?; + http_client_builder = http_client_builder.add_root_certificate(cert); + } + + let http_client = http_client_builder.build()?; + + Ok(Discovery { + verification_url: url::Url::parse(&url) + .map_err(|e| Error::ConfigError(e.to_string()))?, + http_client, + }) + } +} + +impl Default for DiscoveryBuilder { + fn default() -> Self { + Self::new() + } +} + /// This structure allows Veraison endpoints and service capabilities to be discovered /// dynamically. /// /// Use [`Discovery::from_base_url()`] to create an instance of this structure for the /// Veraison service instance that you are communicating with. +/// +/// Currently, only discovery of the verification API is implemented. pub struct Discovery { - provisioning_url: url::Url, //TODO: The provisioning URL discovery is not implemented yet. verification_url: url::Url, - http_client: reqwest::blocking::Client, + http_client: reqwest::Client, } impl Discovery { @@ -421,29 +525,27 @@ impl Discovery { let base_url = url::Url::parse(&base_url_str).map_err(|e| Error::ConfigError(e.to_string()))?; - let mut provisioning_url = base_url.clone(); - provisioning_url.set_path(".well-known/veraison/provisioning"); - let mut verification_url = base_url; verification_url.set_path(".well-known/veraison/verification"); Ok(Discovery { - provisioning_url, verification_url, - http_client: reqwest::blocking::Client::builder().build()?, + http_client: reqwest::Client::new(), }) } /// Obtains the capabilities and endpoints of the Veraison verification service. - pub fn get_verification_api(&self) -> Result { + pub async fn get_verification_api(&self) -> Result { let response = self .http_client .get(self.verification_url.as_str()) .header(reqwest::header::ACCEPT, DISCOVERY_MEDIA_TYPE) - .send()?; + .send() + .await + .unwrap(); match response.status() { - reqwest::StatusCode::OK => Ok(response.json::()?), + reqwest::StatusCode::OK => Ok(response.json::().await?), _ => Err(Error::ApiError(String::from( "Failed to discover verification endpoint information.", ))), @@ -536,7 +638,7 @@ mod tests { .build() .unwrap(); - let rv = cr.new_session(&nonce).expect("unexpected failure"); + let rv = cr.new_session(&nonce).await.expect("unexpected failure"); // Expect we are given the expected location URL assert_eq!(rv.0, format!("{}/1234", mock_server.uri())); @@ -578,6 +680,7 @@ mod tests { let rv = cr .challenge_response(&evidence_value, media_type, &session_url) + .await .expect("unexpected failure"); // Expect we are given the expected attestation result @@ -626,6 +729,7 @@ mod tests { let verification_api = discovery .get_verification_api() + .await .expect("Failed to get verification endpoint details."); // Check that we've pulled and deserialized everything that we expect