Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: asynchronous API #22

Merged
merged 1 commit into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ 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"] }
tokio = { version = "1.43.0", features = ["full"] }

[dependencies.serde_with]
version = "3.11.0"
features = ["base64", "chrono"]

[dev-dependencies]
wiremock = "0.6.2"
async-std = { version = "1.6.5", features = ["attributes"] }
async-std = { version = "1.6.5", features = ["attributes", "tokio1"] }
25 changes: 18 additions & 7 deletions examples/challenge_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,40 @@ extern crate veraison_apiclient;

use veraison_apiclient::*;

fn my_evidence_builder(nonce: &[u8], accept: &[String]) -> Result<(Vec<u8>, String), Error> {
fn my_evidence_builder(
nonce: &[u8],
accept: &[String],
token: Vec<u8>,
) -> Result<(Vec<u8>, 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() {
#[async_std::main]
async fn main() {
let base_url = "https://localhost:8080";

let discovery = DiscoveryBuilder::new()
.with_base_url(base_url.into())
.with_root_certificate("veraison-root.crt".into())
.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
Expand All @@ -39,14 +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())
.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),
}
Expand Down
65 changes: 39 additions & 26 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright 2022 Contributors to the Veraison project.
// Copyright 2022-2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

#![allow(clippy::multiple_crate_versions)]

use std::{fs::File, io::Read, path::PathBuf};

use reqwest::{blocking::ClientBuilder, Certificate};
use reqwest::{Certificate, ClientBuilder};

#[derive(thiserror::Error, PartialEq, Eq)]
pub enum Error {
Expand Down Expand Up @@ -59,7 +59,8 @@ 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<u8>, String), Error>;
type EvidenceCreationCb =
fn(nonce: &[u8], accepted: &[String], token: Vec<u8>) -> Result<(Vec<u8>, String), Error>;

/// A builder for ChallengeResponse objects
pub struct ChallengeResponseBuilder {
Expand Down Expand Up @@ -99,7 +100,7 @@ impl ChallengeResponseBuilder {
.new_session_url
.ok_or_else(|| Error::ConfigError("missing API endpoint".to_string()))?;

let mut http_client_builder: ClientBuilder = reqwest::blocking::ClientBuilder::new();
let mut http_client_builder: ClientBuilder = reqwest::ClientBuilder::new();

if self.root_certificate.is_some() {
let mut buf = Vec::new();
Expand Down Expand Up @@ -128,7 +129,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
Expand All @@ -143,19 +144,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<u8>,
) -> Result<String, Error> {
// 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)
Expand All @@ -164,9 +169,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
Expand All @@ -180,7 +188,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: {}",
Expand All @@ -206,13 +214,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,
Expand All @@ -225,14 +233,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!(
Expand All @@ -259,7 +268,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: {}",
Expand All @@ -268,14 +277,15 @@ impl ChallengeResponse {
}
}

fn new_session_request(&self, nonce: &Nonce) -> Result<reqwest::blocking::Response, Error> {
async fn new_session_request(&self, nonce: &Nonce) -> Result<reqwest::Response, Error> {
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?;

Ok(r)
}
Expand Down Expand Up @@ -403,7 +413,7 @@ impl DiscoveryBuilder {
.url
.ok_or_else(|| Error::ConfigError("missing API endpoint".to_string()))?;

let mut http_client_builder: ClientBuilder = reqwest::blocking::ClientBuilder::new();
let mut http_client_builder: ClientBuilder = reqwest::ClientBuilder::new();

if self.root_certificate.is_some() {
let mut buf = Vec::new();
Expand Down Expand Up @@ -508,7 +518,7 @@ impl VerificationApi {
/// Veraison service instance that you are communicating with.
pub struct Discovery {
verification_url: url::Url,
http_client: reqwest::blocking::Client,
http_client: reqwest::Client,
}

impl Discovery {
Expand All @@ -524,20 +534,21 @@ impl Discovery {

Ok(Discovery {
verification_url,
http_client: reqwest::blocking::Client::new(),
http_client: reqwest::Client::new(),
})
}

/// Obtains the capabilities and endpoints of the Veraison verification service.
pub fn get_verification_api(&self) -> Result<VerificationApi, Error> {
pub async fn get_verification_api(&self) -> Result<VerificationApi, Error> {
let response = self
.http_client
.get(self.verification_url.as_str())
.header(reqwest::header::ACCEPT, DISCOVERY_MEDIA_TYPE)
.send()?;
.send()
.await?;

match response.status() {
reqwest::StatusCode::OK => Ok(response.json::<VerificationApi>()?),
reqwest::StatusCode::OK => Ok(response.json::<VerificationApi>().await?),
_ => Err(Error::ApiError(String::from(
"Failed to discover verification endpoint information.",
))),
Expand Down Expand Up @@ -630,7 +641,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()));
Expand Down Expand Up @@ -672,6 +683,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
Expand Down Expand Up @@ -722,6 +734,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
Expand Down
Loading