use serde::Serialize;
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};
use super::Transport;
use super::VtaClient;
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> {
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> {
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,
};
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 post_trust_task<B, R>(
&self,
type_uri: &'static str,
payload: B,
) -> Result<R, VtaError>
where
B: Serialize,
R: DeserializeOwned,
{
match &self.transport {
Transport::Rest { .. } => {}
#[cfg(feature = "tsp")]
Transport::Tsp { .. } => {
return Err(VtaError::Validation(
"backup descriptor pattern is REST-only; \
this client is on TSP transport"
.into(),
));
}
#[cfg(feature = "session")]
Transport::DIDComm { .. } => {
return Err(VtaError::Validation(
"backup descriptor pattern is REST-only; \
this client is on DIDComm transport"
.into(),
));
}
}
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::Validation(
"DIDComm transport has no REST client for blob download".into(),
)
})?,
#[cfg(feature = "tsp")]
Transport::Tsp { rest_client, .. } => rest_client.as_ref().ok_or_else(|| {
VtaError::Validation("TSP transport has no REST client for blob download".into())
})?,
};
let resp = client
.get(transport_url)
.header(TOKEN_HEADER, transport_token)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(VtaError::from_http(status, body));
}
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::Validation("DIDComm transport has no REST client for blob upload".into())
})?,
#[cfg(feature = "tsp")]
Transport::Tsp { rest_client, .. } => rest_client.as_ref().ok_or_else(|| {
VtaError::Validation("TSP transport has no REST client for blob upload".into())
})?,
};
let resp = client
.post(transport_url)
.header(TOKEN_HEADER, transport_token)
.body(bytes.to_vec())
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(VtaError::from_http(status, body));
}
Ok(())
}
}
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;
}
}