use serde::Serialize;
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};
use super::VtaClient;
use super::backup_chunked::TransferProgress;
use super::{SurfaceTransport, Transport};
use crate::error::VtaError;
use crate::protocols::backup_management::descriptors::{
AbortBundleBody, AbortBundleResultBody, CompleteExportBody, CompleteExportResultBody,
FinalizeImportBody, FinalizeImportResultBody, InitiateExportBody, InitiateExportResultBody,
InitiateImportBody, InitiateImportResultBody,
};
const TOKEN_HEADER: &str = "X-Backup-Token";
impl VtaClient {
pub async fn backup_export_via_descriptor(
&self,
password: &str,
include_audit: bool,
) -> Result<Vec<u8>, VtaError> {
self.backup_export_with_progress(password, include_audit, &mut |_| {})
.await
}
pub async fn backup_export_with_progress(
&self,
password: &str,
include_audit: bool,
progress: &mut (dyn FnMut(TransferProgress) + Send),
) -> Result<Vec<u8>, VtaError> {
if self.trust_task_transport() != SurfaceTransport::Rest {
let (bytes, bundle_id) = self
.backup_export_chunked(password, include_audit, progress)
.await?;
let _: Result<CompleteExportResultBody, VtaError> = self
.post_trust_task(
crate::trust_tasks::TASK_BACKUP_COMPLETE_EXPORT_1_0,
CompleteExportBody { bundle_id },
)
.await;
return Ok(bytes);
}
descriptor_transport_gate(self.trust_task_transport())?;
let req = InitiateExportBody {
password: password.to_string(),
include_audit,
algorithm: "stream".into(),
};
let result: InitiateExportResultBody = self
.post_trust_task(crate::trust_tasks::TASK_BACKUP_INITIATE_EXPORT_1_0, req)
.await?;
let bytes = self
.download_blob(
&result.descriptor.transport_url,
&result.descriptor.transport_token,
)
.await?;
let actual = sha256_hex(&bytes);
if actual != result.descriptor.expected_sha256 {
return Err(VtaError::Protocol(format!(
"downloaded backup hash mismatch: expected {} got {}",
result.descriptor.expected_sha256, actual
)));
}
if bytes.len() as u64 != result.descriptor.expected_size_bytes {
return Err(VtaError::Protocol(format!(
"downloaded backup size mismatch: expected {} got {}",
result.descriptor.expected_size_bytes,
bytes.len()
)));
}
let ack_req = CompleteExportBody {
bundle_id: result.descriptor.bundle_id.clone(),
};
let _: Result<CompleteExportResultBody, VtaError> = self
.post_trust_task(crate::trust_tasks::TASK_BACKUP_COMPLETE_EXPORT_1_0, ack_req)
.await;
Ok(bytes)
}
pub async fn backup_import_via_descriptor(
&self,
bytes: &[u8],
password: &str,
confirm: bool,
) -> Result<FinalizeImportResultBody, VtaError> {
self.backup_import_with_progress(bytes, password, confirm, &mut |_| {})
.await
}
pub async fn backup_import_with_progress(
&self,
bytes: &[u8],
password: &str,
confirm: bool,
progress: &mut (dyn FnMut(TransferProgress) + Send),
) -> Result<FinalizeImportResultBody, VtaError> {
self.backup_import_with_options(bytes, password, confirm, false, progress)
.await
}
pub async fn backup_import_with_options(
&self,
bytes: &[u8],
password: &str,
confirm: bool,
replace_identity: bool,
progress: &mut (dyn FnMut(TransferProgress) + Send),
) -> Result<FinalizeImportResultBody, VtaError> {
if self.trust_task_transport() != SurfaceTransport::Rest {
let bundle_id = self.backup_import_chunked(bytes, progress).await?;
return self
.backup_finalize_import_with(&bundle_id, password, confirm, replace_identity)
.await;
}
descriptor_transport_gate(self.trust_task_transport())?;
let expected_sha256 = sha256_hex(bytes);
let init_req = InitiateImportBody {
expected_sha256,
expected_size_bytes: bytes.len() as u64,
algorithm: "stream".into(),
};
let result: InitiateImportResultBody = self
.post_trust_task(
crate::trust_tasks::TASK_BACKUP_INITIATE_IMPORT_1_0,
init_req,
)
.await?;
self.upload_blob(
&result.descriptor.transport_url,
&result.descriptor.transport_token,
bytes,
)
.await?;
let finalize_req = FinalizeImportBody {
bundle_id: result.descriptor.bundle_id.clone(),
password: password.to_string(),
confirm,
ext: FinalizeImportBody::replace_identity_ext(replace_identity),
};
self.post_trust_task(
crate::trust_tasks::TASK_BACKUP_FINALIZE_IMPORT_1_0,
finalize_req,
)
.await
}
pub async fn backup_abort_bundle(
&self,
bundle_id: &str,
) -> Result<AbortBundleResultBody, VtaError> {
let req = AbortBundleBody {
bundle_id: bundle_id.to_string(),
};
self.post_trust_task(crate::trust_tasks::TASK_BACKUP_ABORT_1_0, req)
.await
}
pub async fn backup_finalize_import(
&self,
bundle_id: &str,
password: &str,
confirm: bool,
) -> Result<FinalizeImportResultBody, VtaError> {
self.backup_finalize_import_with(bundle_id, password, confirm, false)
.await
}
pub async fn backup_finalize_import_with(
&self,
bundle_id: &str,
password: &str,
confirm: bool,
replace_identity: bool,
) -> Result<FinalizeImportResultBody, VtaError> {
let req = FinalizeImportBody {
bundle_id: bundle_id.to_string(),
password: password.to_string(),
confirm,
ext: FinalizeImportBody::replace_identity_ext(replace_identity),
};
let uri = if self.trust_task_transport() == SurfaceTransport::Rest {
crate::trust_tasks::TASK_BACKUP_FINALIZE_IMPORT_1_0
} else {
crate::trust_tasks::TASK_BACKUP_FINALIZE_IMPORT_1_1
};
self.post_trust_task(uri, req).await
}
pub async fn post_trust_task<B, R>(
&self,
type_uri: &'static str,
payload: B,
) -> Result<R, VtaError>
where
B: Serialize,
R: DeserializeOwned,
{
let payload_value = serde_json::to_value(&payload)?;
let response_payload = self
.dispatch_trust_task(type_uri, payload_value, 30)
.await?;
Ok(serde_json::from_value(response_payload)?)
}
pub async fn download_blob(
&self,
transport_url: &str,
transport_token: &str,
) -> Result<Vec<u8>, VtaError> {
let client = match &self.transport {
Transport::Rest { client, .. } => client,
#[cfg(feature = "session")]
Transport::DIDComm { rest_client, .. } => rest_client.as_ref().ok_or_else(|| {
VtaError::UnsupportedTransport(no_rest_leg("DIDComm", "download"))
})?,
#[cfg(feature = "tsp")]
Transport::Tsp { rest_client, .. } => rest_client
.as_ref()
.ok_or_else(|| VtaError::UnsupportedTransport(no_rest_leg("TSP", "download")))?,
};
let resp = client
.get(transport_url)
.header(TOKEN_HEADER, transport_token)
.send()
.await?;
if !resp.status().is_success() {
return Err(VtaError::from_response(resp).await);
}
Ok(resp.bytes().await?.to_vec())
}
pub async fn upload_blob(
&self,
transport_url: &str,
transport_token: &str,
bytes: &[u8],
) -> Result<(), VtaError> {
let client = match &self.transport {
Transport::Rest { client, .. } => client,
#[cfg(feature = "session")]
Transport::DIDComm { rest_client, .. } => rest_client
.as_ref()
.ok_or_else(|| VtaError::UnsupportedTransport(no_rest_leg("DIDComm", "upload")))?,
#[cfg(feature = "tsp")]
Transport::Tsp { rest_client, .. } => rest_client
.as_ref()
.ok_or_else(|| VtaError::UnsupportedTransport(no_rest_leg("TSP", "upload")))?,
};
let resp = client
.post(transport_url)
.header(TOKEN_HEADER, transport_token)
.body(bytes.to_vec())
.send()
.await?;
if !resp.status().is_success() {
return Err(VtaError::from_response(resp).await);
}
Ok(())
}
}
pub(crate) fn descriptor_transport_gate(surface: SurfaceTransport) -> Result<(), VtaError> {
match surface {
SurfaceTransport::Rest => Ok(()),
other => Err(VtaError::UnsupportedTransport(format!(
"the backup `stream` algorithm moves the bytes over the VTA's HTTPS blob \
endpoint; this client's Trust-Task surface is on {other}, which uses the \
`chunkedTrustTask` algorithm instead. Re-run with `--transport rest` to use \
`stream` against a VTA that advertises REST"
))),
}
}
fn no_rest_leg(transport: &str, direction: &str) -> String {
format!(
"{transport} transport has no REST client for the backup blob {direction}; \
re-run with `--transport rest`"
)
}
fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let mut s = String::with_capacity(digest.len() * 2);
for b in digest {
s.push_str(&format!("{b:02x}"));
}
s
}
#[cfg(test)]
mod descriptor_envelope_tests {
use super::super::{ClientIdentity, VtaClient};
use crate::protocols::backup_management::descriptors::{
InitiateExportBody, InitiateImportBody,
};
use crate::trust_tasks;
const VTA_DID: &str = "did:key:z6MkVtaBackupTarget";
fn caller_identity() -> ClientIdentity {
let seed = [0xb4u8; 32];
let sk = ed25519_dalek::SigningKey::from_bytes(&seed);
let client_did = format!(
"did:key:{}",
crate::did_key::ed25519_multibase_pubkey(&sk.verifying_key().to_bytes())
);
let mut buf = vec![0x80, 0x26];
buf.extend_from_slice(&seed);
let private_key_multibase = multibase::encode(multibase::Base::Base58Btc, &buf);
ClientIdentity {
client_did,
private_key_multibase,
vta_did: VTA_DID.to_string(),
verification_method: None,
}
}
async fn assert_conforming_request(type_uri: &'static str, payload: serde_json::Value) {
let id = caller_identity();
let client = VtaClient::new("http://vta.invalid").with_identity(id.clone());
VtaClient::check_payload_conforms(type_uri, &payload)
.unwrap_or_else(|e| panic!("{type_uri}: the payload does not conform: {e}"));
let doc = client
.signed_task_document(type_uri, payload)
.await
.unwrap_or_else(|e| panic!("{type_uri}: building the request failed: {e}"));
assert_eq!(
doc.get("recipient").and_then(|v| v.as_str()),
Some(id.vta_did.as_str()),
"{type_uri}: recipient must be the VTA DID (SPEC §7.2 item 5b)"
);
assert_eq!(
doc.get("issuer").and_then(|v| v.as_str()),
Some(id.client_did.as_str()),
"{type_uri}: issuer must be the caller DID (item 6)"
);
let typed: trust_tasks_rs::TrustTask<serde_json::Value> =
serde_json::from_value(doc).expect("the built document is a TrustTask");
let signer = crate::trust_task_proof::verify_trust_task_proof(&typed)
.await
.unwrap_or_else(|e| panic!("{type_uri}: the proof does not verify: {e:?}"));
assert_eq!(
signer, id.client_did,
"{type_uri}: the proof must be signed by the caller's key"
);
let policy = trust_tasks_rs::schema_index::spec_policy_for(type_uri)
.unwrap_or_else(|| panic!("{type_uri} has no published policy"));
policy
.enforce(&typed)
.unwrap_or_else(|r| panic!("{type_uri}: a conforming VTA would refuse this: {r:?}"));
}
#[tokio::test]
async fn initiate_export_request_is_addressed_and_signed() {
let body = InitiateExportBody {
password: "correct horse battery staple".into(),
include_audit: true,
algorithm: "stream".into(),
};
assert_conforming_request(
trust_tasks::TASK_BACKUP_INITIATE_EXPORT_1_0,
serde_json::to_value(body).unwrap(),
)
.await;
}
#[tokio::test]
async fn initiate_import_request_is_addressed_and_signed() {
let body = InitiateImportBody {
expected_sha256: super::sha256_hex(b"a backup blob"),
expected_size_bytes: 13,
algorithm: "stream".into(),
};
assert_conforming_request(
trust_tasks::TASK_BACKUP_INITIATE_IMPORT_1_0,
serde_json::to_value(body).unwrap(),
)
.await;
}
}
#[cfg(test)]
mod transport_gate_tests {
use super::super::{SurfaceTransport, VtaClient};
use super::descriptor_transport_gate;
use crate::error::VtaError;
#[test]
fn rest_surface_passes_the_gate() {
descriptor_transport_gate(SurfaceTransport::Rest).expect("REST is the supported surface");
}
#[test]
fn mediator_surfaces_are_unsupported_transport_not_validation() {
for surface in [SurfaceTransport::Didcomm, SurfaceTransport::Tsp] {
match descriptor_transport_gate(surface) {
Err(VtaError::UnsupportedTransport(msg)) => {
assert!(
msg.contains("--transport rest"),
"{surface}: the refusal must name the fix, got: {msg}"
);
assert!(
msg.contains(&surface.to_string()),
"{surface}: the refusal must name the surface, got: {msg}"
);
}
other => panic!("{surface}: expected UnsupportedTransport, got {other:?}"),
}
}
}
#[tokio::test]
async fn rest_client_is_not_refused_by_the_gate() {
let client = VtaClient::new("http://127.0.0.1:9");
let err = client
.backup_abort_bundle("3f2504e0-4f89-41d3-9a0c-0305e82c3301")
.await
.expect_err("nothing listens there");
assert!(
!matches!(err, VtaError::UnsupportedTransport(_)),
"a REST client must pass the transport gate, got {err:?}"
);
}
}