diff --git a/Cargo.toml b/Cargo.toml index 50968fe..5b1f245 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "veraison-apiclient" -version = "0.0.1" +version = "0.0.2" edition = "2021" repository = "https://github.com/veraison/rust-apiclient" readme = "README.md" @@ -12,18 +12,18 @@ categories = ["web-programming"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "0.11", features = ["json", "rustls-tls", "blocking"] } +reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking"] } url = { version = "2", features = ["serde"] } base64 = "0.13.0" -thiserror = "1" -serde = "1.0.144" +thiserror = "2.0.6" +serde = "1.0.216" chrono = { version = "0.4", default-features = false, features = ["serde"] } jsonwebkey = { version = "0.3.5", features = ["pkcs-convert"] } [dependencies.serde_with] -version = "1.14.0" +version = "3.11.0" features = ["base64", "chrono"] [dev-dependencies] -wiremock = "0.5" +wiremock = "0.6.2" async-std = { version = "1.6.5", features = ["attributes"] } diff --git a/examples/challenge_response.rs b/examples/challenge_response.rs index e5e089e..c893a2f 100644 --- a/examples/challenge_response.rs +++ b/examples/challenge_response.rs @@ -18,24 +18,30 @@ fn my_evidence_builder(nonce: &[u8], accept: &[String]) -> Result<(Vec, Stri } fn main() { - let base_url = "http://127.0.0.1:8080"; + let base_url = "https://localhost:8080"; - let discovery = Discovery::from_base_url(String::from(base_url)) - .expect("Failed to start API discovery with the service."); + let discovery_api_endpoint = format!("{}{}", base_url, "/.well-known/veraison/verification"); + + 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() - .expect("Failed to discover the verification endpoint details."); + .expect("Failed to discover the verification endpoint details"); let relative_endpoint = verification_api .get_api_endpoint("newChallengeResponseSession") - .expect("Could not locate a newChallengeResponseSession endpoint."); + .expect("Could not locate a newChallengeResponseSession endpoint"); let api_endpoint = format!("{}{}", base_url, relative_endpoint); // create a ChallengeResponse object let cr = ChallengeResponseBuilder::new() .with_new_session_url(api_endpoint) + .with_root_certificate("veraison-root.crt".into()) .build() .unwrap(); diff --git a/examples/veraison-root.crt b/examples/veraison-root.crt new file mode 100644 index 0000000..e894c3d --- /dev/null +++ b/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/src/lib.rs b/src/lib.rs index 401c718..af02b1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,10 @@ #![allow(clippy::multiple_crate_versions)] +use std::{fs::File, io::Read, path::PathBuf}; + +use reqwest::{blocking::ClientBuilder, Certificate}; + #[derive(thiserror::Error, PartialEq, Eq)] pub enum Error { #[error("configuration error: {0}")] @@ -25,6 +29,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()) @@ -54,7 +64,7 @@ type EvidenceCreationCb = fn(nonce: &[u8], accepted: &[String]) -> Result<(Vec, - // TODO(tho) add TLS config / authn tokens etc. + root_certificate: Option, } impl ChallengeResponseBuilder { @@ -62,27 +72,48 @@ impl ChallengeResponseBuilder { pub fn new() -> Self { Self { new_session_url: None, + root_certificate: None, } } /// Use this method to supply the URL of the verification endpoint that will create - /// new challenge-response sessions, e.g. + /// new challenge-response sessions, e.g.: /// "https://veraison.example/challenge-response/v1/newSession". pub fn with_new_session_url(mut self, v: String) -> ChallengeResponseBuilder { self.new_session_url = Some(v); self } + /// Use this method to add a custom root certificate. For example, this can + /// be used to connect to a server that has a self-signed certificate which + /// is not present in (and does not need to be added to) the system's trust + /// anchor store. + 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::blocking::ClientBuilder::new(); + + 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.use_rustls_tls().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, }) } } @@ -330,6 +361,68 @@ pub struct VerificationApi { api_endpoints: std::collections::HashMap, } +/// A builder for Discovery 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, e.g.: + /// "https://veraison.example/.well-known/veraison/verification" + pub fn with_url(mut self, v: String) -> DiscoveryBuilder { + self.url = Some(v); + self + } + + /// Use this method to add a custom root certificate. For example, this can + /// be used to connect to a server that has a self-signed certificate which + /// is not present in (and does not need to be added to) the system's trust + /// anchor store. + 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::blocking::ClientBuilder::new(); + + 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.use_rustls_tls().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() + } +} + impl VerificationApi { /// Obtains the EAR verification public key encoded in ASN.1 DER format. pub fn ear_verification_key_as_der(&self) -> Result, Error> { @@ -406,31 +499,27 @@ impl VerificationApi { /// 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 +/// Use [`DiscoveryBuilder`] to create an instance of this structure for the /// Veraison service instance that you are communicating with. 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, } impl Discovery { + #[deprecated(since = "0.0.2", note = "please use the `DiscoveryBuilder` instead")] /// Establishes client API discovery for the Veraison service instance running at the /// given base URL. pub fn from_base_url(base_url_str: String) -> Result { 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::blocking::Client::new(), }) } @@ -621,7 +710,15 @@ mod tests { .mount(&mock_server) .await; - let discovery = Discovery::from_base_url(mock_server.uri()) + let discovery_api_endpoint = format!( + "{}{}", + mock_server.uri(), + "/.well-known/veraison/verification" + ); + + let discovery = DiscoveryBuilder::new() + .with_url(discovery_api_endpoint) + .build() .expect("Failed to create Discovery client."); let verification_api = discovery