diff --git a/Cargo.lock b/Cargo.lock index 1698af3a5..58b064b24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,7 +290,7 @@ dependencies = [ "futures-lite", "libc", "once_cell", - "signal-hook", + "signal-hook 0.3.9", "winapi 0.3.9", ] @@ -793,6 +793,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.19" @@ -1002,6 +1008,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio 0.7.13", + "parking_lot 0.11.1", + "signal-hook 0.1.17", + "winapi 0.3.9", +] + +[[package]] +name = "crossterm_winapi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -2175,6 +2206,7 @@ dependencies = [ "humanode-rpc", "humanode-runtime", "parity-scale-codec", + "qr2term", "reqwest", "robonode-client", "sc-basic-authorship", @@ -2192,6 +2224,7 @@ dependencies = [ "sp-timestamp", "tokio 1.6.2", "tracing", + "url 2.2.2", ] [[package]] @@ -4816,6 +4849,25 @@ dependencies = [ "parity-wasm 0.42.2", ] +[[package]] +name = "qr2term" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9634478e874d4153c52d10d6f8660ad1b17be5d5a48aa7286f3d52fd2c9b7c34" +dependencies = [ + "crossterm", + "qrcode", +] + +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -6455,6 +6507,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" +[[package]] +name = "signal-hook" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +dependencies = [ + "libc", + "mio 0.7.13", + "signal-hook-registry", +] + [[package]] name = "signal-hook" version = "0.3.9" diff --git a/crates/bioauth-flow/src/flow.rs b/crates/bioauth-flow/src/flow.rs index 733decb3b..b37f47aa4 100644 --- a/crates/bioauth-flow/src/flow.rs +++ b/crates/bioauth-flow/src/flow.rs @@ -1,6 +1,6 @@ //! The flow implementation. -use std::marker::PhantomData; +use std::{marker::PhantomData, ops::Deref}; use primitives_liveness_data::{LivenessData, OpaqueLivenessData}; use robonode_client::{AuthenticateRequest, EnrollRequest}; @@ -37,16 +37,16 @@ pub trait Signer { /// /// The goal of this component is to encapsulate interoperation with the handheld device /// and the robonode. -pub struct Flow { +pub struct Flow { /// The provider of the liveness data. pub liveness_data_provider: LDP, /// The Robonode API client. - pub robonode_client: robonode_client::Client, + pub robonode_client: RC, /// The type used to encode the public key. - pub public_key_type: PhantomData, + pub validator_public_key_type: PhantomData, } -impl Flow +impl Flow where LDP: LivenessDataProvider, { @@ -60,11 +60,12 @@ where } } -impl Flow +impl Flow where PK: AsRef<[u8]>, LDP: LivenessDataProvider, ::Error: Send + Sync + std::error::Error + 'static, + RC: Deref, { /// The enroll flow. pub async fn enroll(&mut self, public_key: PK) -> Result<(), anyhow::Error> { @@ -81,12 +82,13 @@ where } } -impl Flow +impl Flow where PK: Signer>, >>::Error: Send + Sync + std::error::Error + 'static, LDP: LivenessDataProvider, ::Error: Send + Sync + std::error::Error + 'static, + RC: Deref, { /// The authentication flow. /// diff --git a/crates/humanode-peer/Cargo.toml b/crates/humanode-peer/Cargo.toml index 0cb5f0ea0..843521890 100644 --- a/crates/humanode-peer/Cargo.toml +++ b/crates/humanode-peer/Cargo.toml @@ -15,6 +15,7 @@ async-trait = "0.1" codec = { package = "parity-scale-codec", version = "2.0.0" } futures = "0.3" hex-literal = "0.3" +qr2term = "0.2" reqwest = "0.11" sc-basic-authorship = { git = "https://github.com/humanode-network/substrate", branch = "master" } sc-client-api = { git = "https://github.com/humanode-network/substrate", branch = "master" } @@ -31,3 +32,4 @@ sp-runtime = { git = "https://github.com/humanode-network/substrate", branch = " sp-timestamp = { git = "https://github.com/humanode-network/substrate", branch = "master" } tokio = { version = "1", features = ["full"] } tracing = "0.1" +url = "2" diff --git a/crates/humanode-peer/src/main.rs b/crates/humanode-peer/src/main.rs index e810fd27e..7a6db3fd5 100644 --- a/crates/humanode-peer/src/main.rs +++ b/crates/humanode-peer/src/main.rs @@ -10,14 +10,16 @@ use sc_tracing::logging::LoggerBuilder; mod chain_spec; mod config; +mod qrcode; mod service; +mod validator_key; #[tokio::main] async fn main() { let logger = LoggerBuilder::new(""); logger.init().unwrap(); - let mut task_manager = service::new_full(config::make()).unwrap(); + let mut task_manager = service::new_full(config::make()).await.unwrap(); tokio::select! { res = task_manager.future() => res.unwrap(), diff --git a/crates/humanode-peer/src/qrcode.rs b/crates/humanode-peer/src/qrcode.rs new file mode 100644 index 000000000..25290dda5 --- /dev/null +++ b/crates/humanode-peer/src/qrcode.rs @@ -0,0 +1,44 @@ +//! QR Code generation. + +use tracing::{error, info}; +use url::Url; + +/// The information necessary for printing the Web App QR Code. +pub struct WebApp { + /// The Web App URL. + url: Url, +} + +impl WebApp { + /// Create a new [`WebApp`] and validate that the resulting URL is valid. + pub fn new(base_url: &str, rpc_url: &str) -> Result { + let mut url = Url::parse(base_url).map_err(|err| err.to_string())?; + url.path_segments_mut() + .map_err(|_| "invalid base URL".to_owned())? + .push("humanode") + .push(rpc_url); + Ok(Self { url }) + } + + /// Print the QR Code for the Web App to the terminal. + pub fn print(&self) { + info!("Please visit {} to proceed", self.url); + qr2term::print_qr(self.url.as_str()) + .unwrap_or_else(|error| error!(message = "Failed to generate QR Code", %error)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_url_construction() { + let webapp = WebApp::new("https://example.com", "http://localhost:9933").unwrap(); + + assert_eq!( + webapp.url, + Url::parse("https://example.com/humanode/http:%2F%2Flocalhost:9933").unwrap() + ); + } +} diff --git a/crates/humanode-peer/src/service.rs b/crates/humanode-peer/src/service.rs index d24a5de51..a30ac4505 100644 --- a/crates/humanode-peer/src/service.rs +++ b/crates/humanode-peer/src/service.rs @@ -1,6 +1,6 @@ //! Initializing, bootstrapping and launching the node from a provided configuration. -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; use humanode_runtime::{self, opaque::Block, RuntimeApi}; use sc_client_api::ExecutorProvider; @@ -10,6 +10,7 @@ pub use sc_executor::NativeExecutor; use sc_service::{Configuration, Error as ServiceError, TaskManager}; use sp_consensus::SlotData; use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; +use tracing::*; // Native executor for the runtime based on the runtime API that is available // at the current compile time. @@ -21,7 +22,7 @@ native_executor_instance!( /// Create a "full" node (full is in terms of substrate). /// We don't support other node types yet either way, so this is the only way to create a node. -pub fn new_full(config: Configuration) -> Result { +pub async fn new_full(config: Configuration) -> Result { let (client, backend, keystore_container, mut task_manager) = sc_service::new_full_parts::(&config, None)?; let client = Arc::new(client); @@ -93,7 +94,7 @@ pub fn new_full(config: Configuration) -> Result { reqwest: reqwest::Client::new(), }); - let (bioauth_flow_rpc_slot, _bioauth_flow_provider_slot) = + let (bioauth_flow_rpc_slot, bioauth_flow_provider_slot) = bioauth_flow::rpc::new_liveness_data_tx_slot(); let rpc_extensions_builder = { @@ -113,6 +114,7 @@ pub fn new_full(config: Configuration) -> Result { }) }; + let rpc_port = config.rpc_http.expect("HTTP RPC must be on").port(); let _rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams { network: Arc::clone(&network), client: Arc::clone(&client), @@ -163,5 +165,59 @@ pub fn new_full(config: Configuration) -> Result { network_starter.start_network(); + let mut flow = bioauth_flow::flow::Flow { + liveness_data_provider: bioauth_flow::rpc::Provider::new(bioauth_flow_provider_slot), + robonode_client, + validator_public_key_type: PhantomData::, + }; + + let webapp_url = std::env::var("WEBAPP_URL") + .unwrap_or_else(|_| "https://webapp-test-1.dev.humanode.io".into()); + // TODO: more advanced host address detection is needed to things work within the same LAN. + let rpc_url = + std::env::var("RPC_URL").unwrap_or_else(|_| format!("http://localhost:{}", rpc_port)); + let webapp_qrcode = + crate::qrcode::WebApp::new(&webapp_url, &rpc_url).map_err(ServiceError::Other)?; + + let bioauth_flow_future = Box::pin(async move { + info!("bioauth flow starting up"); + let should_enroll = std::env::var("ENROLL").unwrap_or_default() == "true"; + if should_enroll { + info!("bioauth flow - enrolling in progress"); + + webapp_qrcode.print(); + + flow.enroll(crate::validator_key::FakeTodo("TODO")) + .await + .expect("enroll failed"); + + info!("bioauth flow - enrolling complete"); + } + + info!("bioauth flow - authentication in progress"); + + webapp_qrcode.print(); + + let authenticate_response = loop { + let result = flow + .authenticate(crate::validator_key::FakeTodo("TODO")) + .await; + match result { + Ok(v) => break v, + Err(error) => { + error!(message = "bioauth flow - authentication failure", ?error); + } + }; + }; + + info!("bioauth flow - authentication complete"); + + info!(message = "We've obtained an auth ticket", auth_ticket = ?authenticate_response.auth_ticket); + }); + + task_manager + .spawn_handle() + .spawn_blocking("bioauth-flow", bioauth_flow_future); + Ok(task_manager) } diff --git a/crates/humanode-peer/src/validator_key.rs b/crates/humanode-peer/src/validator_key.rs new file mode 100644 index 000000000..e43d5bf7f --- /dev/null +++ b/crates/humanode-peer/src/validator_key.rs @@ -0,0 +1,28 @@ +//! The validator key integration logic. + +use std::convert::Infallible; + +use bioauth_flow::flow::Signer; + +/// A temporary fake implementation of the validator key, for the purposes of using it with the +/// bioauth enroll and authenticate during the integration while the real validator key is not +/// ready. +pub struct FakeTodo(pub &'static str); + +#[async_trait::async_trait] +impl Signer> for FakeTodo { + type Error = Infallible; + + async fn sign<'a, D>(&self, _data: D) -> Result, Self::Error> + where + D: AsRef<[u8]> + Send + 'a, + { + Ok(b"0123456789abcdef0123456789abcdef"[..].into()) + } +} + +impl AsRef<[u8]> for FakeTodo { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} diff --git a/crates/robonode-server/src/http/filters.rs b/crates/robonode-server/src/http/filters.rs index 5b7dc7a75..aa820e909 100644 --- a/crates/robonode-server/src/http/filters.rs +++ b/crates/robonode-server/src/http/filters.rs @@ -26,7 +26,7 @@ where { // When accepting a body, we want a JSON body // (and to reject huge payloads)... - warp::body::content_length_limit(1024 * 16).and(warp::body::json::()) + warp::body::content_length_limit(1024 * 1024 * 16).and(warp::body::json::()) } /// The root mount point with all the routes. @@ -35,7 +35,7 @@ pub fn root( ) -> impl Filter + Clone where S: Signer> + Send + Sync + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a str> + Verifier> + Into>, + PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]> + Verifier> + Into>, { enroll(Arc::clone(&logic)) .or(authenticate(Arc::clone(&logic))) @@ -49,7 +49,7 @@ fn enroll( ) -> impl Filter + Clone where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]>, { warp::path!("enroll") .and(warp::post()) @@ -64,7 +64,7 @@ fn authenticate( ) -> impl Filter + Clone where S: Signer> + Send + Sync + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a str> + Verifier> + Into>, + PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]> + Verifier> + Into>, { warp::path!("authenticate") .and(warp::post()) @@ -79,7 +79,7 @@ fn get_facetec_session_token( ) -> impl Filter + Clone where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]>, { warp::path!("facetec-session-token") .and(warp::get()) @@ -93,7 +93,7 @@ fn get_facetec_device_sdk_params( ) -> impl Filter + Clone where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]>, { warp::path!("facetec-device-sdk-params") .and(warp::get()) diff --git a/crates/robonode-server/src/http/handlers.rs b/crates/robonode-server/src/http/handlers.rs index 68d99ec67..f1c28b915 100644 --- a/crates/robonode-server/src/http/handlers.rs +++ b/crates/robonode-server/src/http/handlers.rs @@ -14,7 +14,7 @@ pub async fn enroll( ) -> Result where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]>, { match logic.enroll(input).await { Ok(()) => Ok(StatusCode::CREATED), @@ -29,7 +29,7 @@ pub async fn authenticate( ) -> Result where S: Signer> + Send + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a str> + Verifier> + Into>, + PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + Verifier> + Into>, { match logic.authenticate(input).await { Ok(res) => { @@ -45,7 +45,7 @@ pub async fn get_facetec_session_token( ) -> Result where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]>, { match logic.get_facetec_session_token().await { Ok(res) => { @@ -61,7 +61,7 @@ pub async fn get_facetec_device_sdk_params( ) -> Result where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]>, { match logic.get_facetec_device_sdk_params().await { Ok(res) => { diff --git a/crates/robonode-server/src/lib.rs b/crates/robonode-server/src/lib.rs index c2f446309..7c592e103 100644 --- a/crates/robonode-server/src/lib.rs +++ b/crates/robonode-server/src/lib.rs @@ -68,10 +68,10 @@ impl logic::Verifier> for ValidatorPublicKeyToDo { } } -impl std::convert::TryFrom<&str> for ValidatorPublicKeyToDo { +impl std::convert::TryFrom<&[u8]> for ValidatorPublicKeyToDo { type Error = (); - fn try_from(val: &str) -> Result { + fn try_from(val: &[u8]) -> Result { Ok(Self(val.into())) } } @@ -81,3 +81,9 @@ impl From for Vec { val.0 } } + +impl AsRef<[u8]> for ValidatorPublicKeyToDo { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/crates/robonode-server/src/logic.rs b/crates/robonode-server/src/logic.rs index babe3f69b..802eee517 100644 --- a/crates/robonode-server/src/logic.rs +++ b/crates/robonode-server/src/logic.rs @@ -53,11 +53,7 @@ pub struct FacetecDeviceSdkParams { /// The inner state, to be hidden behind the mutex to ensure we don't have /// access to it unless we lock the mutex. -pub struct Locked -where - S: Signer> + 'static, - PK: Send + for<'a> TryFrom<&'a str>, -{ +pub struct Locked { /// The sequence number. pub sequence: Sequence, /// The client for the FaceTec Server API. @@ -69,11 +65,7 @@ where } /// The overall generic logic. -pub struct Logic -where - S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, -{ +pub struct Logic { /// The mutex over the locked portions of the logic. /// This way we're ensureing the operations can only be conducted under /// the lock. @@ -86,7 +78,7 @@ where #[derive(Debug, Deserialize)] pub struct EnrollRequest { /// The public key of the validator. - public_key: String, + public_key: Vec, /// The liveness data that the validator owner provided. liveness_data: OpaqueLivenessData, } @@ -139,22 +131,23 @@ const MATCH_LEVEL: i64 = 10; impl Logic where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]> + AsRef<[u8]>, { /// An enroll invocation handler. pub async fn enroll(&self, req: EnrollRequest) -> Result<(), EnrollError> { - if PK::try_from(&req.public_key).is_err() { - return Err(EnrollError::InvalidPublicKey); - } + let public_key = + PK::try_from(&req.public_key).map_err(|_| EnrollError::InvalidPublicKey)?; let liveness_data = LivenessData::try_from(&req.liveness_data).map_err(EnrollError::InvalidLivenessData)?; + let public_key_hex = hex::encode(public_key); + let unlocked = self.locked.lock().await; let enroll_res = unlocked .facetec .enrollment_3d(Enrollment3DRequest { - external_database_ref_id: &req.public_key, + external_database_ref_id: &public_key_hex, face_scan: &liveness_data.face_scan, audit_trail_image: &liveness_data.audit_trail_image, low_quality_audit_trail_image: &liveness_data.low_quality_audit_trail_image, @@ -183,7 +176,7 @@ where let search_res = unlocked .facetec .db_search(DBSearchRequest { - external_database_ref_id: &req.public_key, + external_database_ref_id: &public_key_hex, group_name: DB_GROUP_NAME, min_match_level: MATCH_LEVEL, }) @@ -203,7 +196,7 @@ where let enroll_res = unlocked .facetec .db_enroll(DBEnrollRequest { - external_database_ref_id: &req.public_key, + external_database_ref_id: &public_key_hex, group_name: "", }) .await @@ -271,6 +264,8 @@ pub enum AuthenticateError { /// Internal error at 3D-DB search due to match-level mismatch in /// the search results. InternalErrorDbSearchMatchLevelMismatch, + /// Internal error at converting public key hex representation to bytes. + InternalErrorInvalidPublicKeyHex, /// Internal error at public key loading due to invalid public key. InternalErrorInvalidPublicKey, /// Internal error at signature verification. @@ -282,7 +277,7 @@ pub enum AuthenticateError { impl Logic where S: Signer> + Send + 'static, - PK: Send + Sync + for<'a> TryFrom<&'a str> + Verifier> + Into>, + PK: Send + Sync + for<'a> TryFrom<&'a [u8]> + Verifier> + Into>, { /// An authenticate invocation handler. pub async fn authenticate( @@ -347,7 +342,9 @@ where return Err(AuthenticateError::InternalErrorDbSearchMatchLevelMismatch); } - let public_key = PK::try_from(&found.identifier) + let public_key_bytes = hex::decode(&found.identifier) + .map_err(|_| AuthenticateError::InternalErrorInvalidPublicKeyHex)?; + let public_key = PK::try_from(&public_key_bytes) .map_err(|_| AuthenticateError::InternalErrorInvalidPublicKey)?; let signature_valid = public_key @@ -408,7 +405,7 @@ pub enum GetFacetecSessionTokenError { impl Logic where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]>, { /// Get a FaceTec Session Token. pub async fn get_facetec_session_token( @@ -448,7 +445,7 @@ pub enum GetFacetecDeviceSdkParamsError {} impl Logic where S: Signer> + Send + 'static, - PK: Send + for<'a> TryFrom<&'a str>, + PK: Send + for<'a> TryFrom<&'a [u8]>, { /// Get the FaceTec Device SDK params . pub async fn get_facetec_device_sdk_params( diff --git a/utils/bioauth-test-tools/facetec-test-data/.gitignore b/utils/bioauth-test-tools/facetec-test-data/.gitignore new file mode 100644 index 000000000..7c9d611b5 --- /dev/null +++ b/utils/bioauth-test-tools/facetec-test-data/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/utils/bioauth-test-tools/facetec-test-data/README.md b/utils/bioauth-test-tools/facetec-test-data/README.md new file mode 100644 index 000000000..0ffb3797b --- /dev/null +++ b/utils/bioauth-test-tools/facetec-test-data/README.md @@ -0,0 +1,11 @@ +# Facetec Test Data + +Place the `FaceTec_Test_Data` directory from the Postman Test Suite in this +directory, such that the files are available under path like: + +- `.../facetec-test-data/FaceTec_Test_Data/3D_FaceScans/liveness_check_subject1_android_scan1.base64.txt` +- `.../facetec-test-data/FaceTec_Test_Data/Audit_Trail_Images/liveness_check_subject1_android_scan1.base64.txt` +- etc... + +Assuming that `.../facetec-test-data` is the directory where this `README.md` +is. diff --git a/utils/bioauth-test-tools/provide-liveness-data b/utils/bioauth-test-tools/provide-liveness-data new file mode 100755 index 000000000..a74c3c403 --- /dev/null +++ b/utils/bioauth-test-tools/provide-liveness-data @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +TEST_FILE_NAME="$1" +URL="${2:-"http://localhost:9933"}" + +TEST_DATA_PATH="facetec-test-data" + +FACE_SCAN_FILE="$TEST_DATA_PATH/FaceTec_Test_Data/3D_FaceScans/$TEST_FILE_NAME" +AUDIT_TRAIL_IMAGE_FILE="$TEST_DATA_PATH/FaceTec_Test_Data/Audit_Trail_Images/$TEST_FILE_NAME" +LOW_QUALITY_AUDIT_TRAIL_IMAGE_FILE="$TEST_DATA_PATH/FaceTec_Test_Data/Low_Quality_Audit_Trail_Images/$TEST_FILE_NAME" + +gen_request() { + cat <<-EOF + { + "jsonrpc":"2.0", + "id":1, + "method":"bioauth_provideLivenessData", + "params": [ + { + "face_scan":"$(cat "$FACE_SCAN_FILE")", + "audit_trail_image":"$(cat "$AUDIT_TRAIL_IMAGE_FILE")", + "low_quality_audit_trail_image":"$(cat "$LOW_QUALITY_AUDIT_TRAIL_IMAGE_FILE")" + } + ] + } +EOF +} + +gen_request | curl -v "$URL" \ + -H "Content-Type:application/json;charset=utf-8" \ + --data-binary @-