diff --git a/libparsec/crates/client/src/client/mod.rs b/libparsec/crates/client/src/client/mod.rs index feca939d2f0..7edb1c78805 100644 --- a/libparsec/crates/client/src/client/mod.rs +++ b/libparsec/crates/client/src/client/mod.rs @@ -6,6 +6,7 @@ mod list_frozen_users; mod organization_info; mod pki_enrollment_accept; mod pki_enrollment_finalize; +mod pki_enrollment_info; mod pki_enrollment_list; mod pki_enrollment_reject; mod pki_enrollment_submit; @@ -53,6 +54,7 @@ pub use self::{ }; use crate::{ certif::{CertifPollServerError, CertificateOps}, + client::pki_enrollment_info::{PKIInfoItem, PkiEnrollmentInfoError}, config::{ClientConfig, ServerConfig}, event_bus::EventBus, monitors::{ @@ -692,6 +694,14 @@ impl Client { pki_enrollment_list::list_enrollments(&self.cmds).await } + pub async fn pki_enrollment_info( + config: Arc, + addr: ParsecPkiEnrollmentAddr, + id: PKIEnrollmentID, + ) -> Result { + pki_enrollment_info::info(config, addr, id).await + } + pub async fn pki_enrollment_reject( &self, enrollment_id: PKIEnrollmentID, diff --git a/libparsec/crates/client/src/client/pki_enrollment_info.rs b/libparsec/crates/client/src/client/pki_enrollment_info.rs new file mode 100644 index 00000000000..e8429609628 --- /dev/null +++ b/libparsec/crates/client/src/client/pki_enrollment_info.rs @@ -0,0 +1,107 @@ +// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS + +use crate::ClientConfig; +pub use anonymous_cmds::latest::pki_enrollment_info::PkiEnrollmentInfoStatus; +use libparsec_client_connection::{protocol::anonymous_cmds, AnonymousCmds, ConnectionError}; +use libparsec_platform_pki::{load_answer_payload, SignedMessage}; +use libparsec_types::prelude::*; +use std::sync::Arc; + +#[derive(Debug, thiserror::Error)] +pub enum PkiEnrollmentInfoError { + #[error("Cannot communicate with the server: {0}")] + Offline(#[from] ConnectionError), + #[error("No enrollment found with that id")] + EnrollmentNotFound, + #[error("Invalid accept payload")] + InvalidAcceptPayload, + #[error(transparent)] + Internal(#[from] anyhow::Error), +} + +#[derive(Debug)] +pub enum PKIInfoItem { + Accepted { + // Deserialized version of the provided payload + // signature should have been checked before loading it + answer: PkiEnrollmentAnswerPayload, + accepted_on: DateTime, + submitted_on: DateTime, + }, + Submitted { + submitted_on: DateTime, + }, + Rejected { + rejected_on: DateTime, + submitted_on: DateTime, + }, + Cancelled { + submitted_on: DateTime, + cancelled_on: DateTime, + }, +} + +pub async fn info( + config: Arc, + addr: ParsecPkiEnrollmentAddr, + enrollment_id: PKIEnrollmentID, +) -> Result { + use anonymous_cmds::latest::pki_enrollment_info::{Rep, Req}; + let cmds = AnonymousCmds::new( + &config.config_dir, + ParsecAnonymousAddr::ParsecPkiEnrollmentAddr(addr.clone()), + config.proxy.clone(), + )?; + let rep = cmds.send(Req { enrollment_id }).await?; + + let status = match rep { + Rep::Ok(status) => status, + Rep::EnrollmentNotFound => return Err(PkiEnrollmentInfoError::EnrollmentNotFound), + rep @ Rep::UnknownStatus { .. } => { + return Err(anyhow::anyhow!("Unexpected server response: {:?}", rep).into()) + } + }; + + // Check that the payload is valid + let answer = match status { + PkiEnrollmentInfoStatus::Submitted { submitted_on } => { + PKIInfoItem::Submitted { submitted_on } + } + PkiEnrollmentInfoStatus::Rejected { + rejected_on, + submitted_on, + } => PKIInfoItem::Rejected { + rejected_on, + submitted_on, + }, + PkiEnrollmentInfoStatus::Cancelled { + submitted_on, + cancelled_on, + } => PKIInfoItem::Cancelled { + submitted_on, + cancelled_on, + }, + PkiEnrollmentInfoStatus::Accepted { + accept_payload, + accept_payload_signature, + accepted_on, + accepter_der_x509_certificate, + submitted_on, + accept_payload_signature_algorithm, + } => { + let message = SignedMessage { + algo: accept_payload_signature_algorithm, + signature: accept_payload_signature.to_vec(), + message: accept_payload.to_vec(), + }; + let answer = load_answer_payload(&accepter_der_x509_certificate, &message, accepted_on) + .map_err(|_| PkiEnrollmentInfoError::InvalidAcceptPayload)?; + PKIInfoItem::Accepted { + answer, + accepted_on, + submitted_on, + } + } + }; + Ok(answer) +} diff --git a/libparsec/crates/client/tests/unit/client/mod.rs b/libparsec/crates/client/tests/unit/client/mod.rs index 7eeb8194f0e..e88892abc0d 100644 --- a/libparsec/crates/client/tests/unit/client/mod.rs +++ b/libparsec/crates/client/tests/unit/client/mod.rs @@ -8,6 +8,7 @@ mod list_users; mod list_workspace_users; mod list_workspaces; mod organization_info; +mod pki_enrollment_info; mod pki_enrollment_list; mod pki_enrollment_reject; mod process_workspaces_needs; diff --git a/libparsec/crates/client/tests/unit/client/pki_enrollment_info.rs b/libparsec/crates/client/tests/unit/client/pki_enrollment_info.rs new file mode 100644 index 00000000000..d7cb3e478e7 --- /dev/null +++ b/libparsec/crates/client/tests/unit/client/pki_enrollment_info.rs @@ -0,0 +1,126 @@ +// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS + +use libparsec_tests_fixtures::prelude::*; +use libparsec_types::prelude::*; + +use crate::{ + client::pki_enrollment_info::{PKIInfoItem, PkiEnrollmentInfoError}, + Client, +}; + +use super::utils::client_factory; +use libparsec_client_connection::test_register_send_hook; +use libparsec_platform_pki::sign_message; +use libparsec_protocol::anonymous_cmds::{self, v5::pki_enrollment_info::PkiEnrollmentInfoStatus}; + +#[parsec_test(testbed = "minimal")] +#[case("submitted")] +#[case("rejected")] +#[case("cancelled")] +// #[case("accepted")] // TODO #11269 when pki set up in testbed +async fn ok(#[case] status: &str, env: &TestbedEnv) { + let alice_device = env.local_device("alice@dev1"); + + let alice_client = client_factory(&env.discriminant_dir, alice_device.clone()).await; + + let expected_submitted_on = DateTime::from_timestamp_micros(1668594983390001).unwrap(); + let enrollment_id = PKIEnrollmentID::from_hex("e1fe88bd0f054261887a6c8039710b40").unwrap(); + + let pki_enrollment_item = match status { + "accepted" => { + let expected_accepted_on = DateTime::from_timestamp_micros(1668594983390002).unwrap(); + + let expected_answer = PkiEnrollmentAnswerPayload { + user_id: UserID::from_hex("9268b5acc07711f0ae7c2394da79527f").unwrap(), + device_id: DeviceID::from_hex("a46105b6c07711f09e41f70f2e4e5650").unwrap(), + device_label: DeviceLabel::try_from("new pki device").unwrap(), + human_handle: HumanHandle::new( + EmailAddress::try_from("pki@invalid.com").unwrap(), + "pki", + ) + .unwrap(), + profile: UserProfile::Standard, + root_verify_key: alice_device.root_verify_key().clone(), + }; + + let cert_hash = X509CertificateHash::fake_sha256(); + let cert_ref = cert_hash.clone().into(); + let signed = sign_message(&expected_answer.dump(), &cert_ref) + .context("Failed to sign message") + .unwrap(); + + PkiEnrollmentInfoStatus::Accepted { + accepter_der_x509_certificate: cert_hash.to_string().into(), + accept_payload: expected_answer.dump().into(), + accept_payload_signature: signed.signature, + accept_payload_signature_algorithm: signed.algo, + submitted_on: expected_submitted_on, + accepted_on: expected_accepted_on, + } + } + "cancelled" => { + let expected_cancelled_on = DateTime::from_timestamp_micros(1668594983390002).unwrap(); + PkiEnrollmentInfoStatus::Cancelled { + cancelled_on: expected_cancelled_on, + submitted_on: expected_submitted_on, + } + } + "rejected" => { + let expected_rejected_on = DateTime::from_timestamp_micros(1668594983390002).unwrap(); + PkiEnrollmentInfoStatus::Rejected { + rejected_on: expected_rejected_on, + submitted_on: expected_submitted_on, + } + } + "submitted" => PkiEnrollmentInfoStatus::Submitted { + submitted_on: expected_submitted_on, + }, + _ => unimplemented!(), + }; + + test_register_send_hook(&env.discriminant_dir, { + move |_req: anonymous_cmds::latest::pki_enrollment_info::Req| { + anonymous_cmds::latest::pki_enrollment_info::Rep::Ok(pki_enrollment_item) + } + }); + + let enrollment_info = Client::pki_enrollment_info( + alice_client.config.clone(), + ParsecPkiEnrollmentAddr::new( + alice_client.organization_addr(), + alice_client.organization_id().clone(), + ), + enrollment_id, + ) + .await + .unwrap(); + match (enrollment_info, status) { + (PKIInfoItem::Accepted { .. }, "accepted") => {} + (PKIInfoItem::Cancelled { .. }, "cancelled") => {} + (PKIInfoItem::Submitted { .. }, "submitted") => {} + (PKIInfoItem::Rejected { .. }, "rejected") => {} + _ => panic!("unexpected answer"), + } +} + +#[parsec_test(testbed = "coolorg")] +async fn enrollment_not_found(env: &TestbedEnv) { + let mallory_device = env.local_device("mallory@dev1"); + let mallory_client = client_factory(&env.discriminant_dir, mallory_device).await; + test_register_send_hook(&env.discriminant_dir, { + move |_req: anonymous_cmds::latest::pki_enrollment_info::Req| { + anonymous_cmds::latest::pki_enrollment_info::Rep::EnrollmentNotFound + } + }); + + let rep = Client::pki_enrollment_info( + mallory_client.config.clone(), + ParsecPkiEnrollmentAddr::new( + mallory_client.organization_addr(), + mallory_client.organization_id().clone(), + ), + PKIEnrollmentID::default(), + ) + .await; + p_assert_matches!(rep.unwrap_err(), PkiEnrollmentInfoError::EnrollmentNotFound) +}