Skip to content

Commit c54e031

Browse files
committed
libparsec_client implementation for async enrollment
1 parent cbcf4ae commit c54e031

File tree

8 files changed

+530
-0
lines changed

8 files changed

+530
-0
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
2+
3+
use libparsec_client_connection::{AuthenticatedCmds, ConnectionError};
4+
use libparsec_types::prelude::*;
5+
6+
use crate::{
7+
greater_timestamp, utils::create_user_and_device_certificates, EventBus,
8+
EventTooMuchDriftWithServerClock, GreaterTimestampOffset,
9+
};
10+
11+
#[derive(Debug, thiserror::Error)]
12+
pub enum AsyncEnrollmentAcceptError {
13+
#[error("Cannot communicate with the server: {0}")]
14+
Offline(#[from] ConnectionError),
15+
16+
#[error("Author not allowed")]
17+
AuthorNotAllowed,
18+
#[error("Enrollment not found")]
19+
EnrollmentNotFound,
20+
#[error("Enrollment no longer available (either already accepted or rejected)")]
21+
EnrollmentNoLongerAvailable,
22+
#[error("Submitter has provided an invalid request: {0}")]
23+
BadSubmitPayload(#[from] AsyncEnrollmentVerifySubmitPayloadError),
24+
#[error("Active users limit reached")]
25+
ActiveUsersLimitReached,
26+
#[error("Human handle (i.e. email address) already taken")]
27+
HumanHandleAlreadyTaken,
28+
#[error("Our clock ({client_timestamp}) and the server's one ({server_timestamp}) are too far apart")]
29+
TimestampOutOfBallpark {
30+
server_timestamp: DateTime,
31+
client_timestamp: DateTime,
32+
ballpark_client_early_offset: f64,
33+
ballpark_client_late_offset: f64,
34+
},
35+
#[error(transparent)]
36+
Internal(#[from] anyhow::Error),
37+
}
38+
39+
pub trait AsyncEnrollmentAcceptIdentityStrategy {
40+
fn verify_submit_payload(
41+
&self,
42+
payload: &[u8],
43+
payload_signature: libparsec_client_connection::protocol::authenticated_cmds::latest::async_enrollment_list::SubmitPayloadSignature,
44+
) -> Result<AsyncEnrollmentSubmitPayload, AsyncEnrollmentVerifySubmitPayloadError> {
45+
let submit_payload = AsyncEnrollmentSubmitPayload::load(payload)
46+
.map_err(|_| AsyncEnrollmentVerifySubmitPayloadError::BadPayload)?;
47+
self._verify_submit_payload(payload, payload_signature)?;
48+
Ok(submit_payload)
49+
}
50+
fn _verify_submit_payload(
51+
&self,
52+
payload: &[u8],
53+
payload_signature: libparsec_client_connection::protocol::authenticated_cmds::latest::async_enrollment_list::SubmitPayloadSignature,
54+
) -> Result<AsyncEnrollmentSubmitPayload, AsyncEnrollmentVerifySubmitPayloadError>;
55+
fn sign_accept_payload(&self, payload: &[u8]) -> libparsec_client_connection::protocol::authenticated_cmds::latest::async_enrollment_accept::AcceptPayloadSignature;
56+
}
57+
58+
#[derive(Debug, thiserror::Error)]
59+
pub enum AsyncEnrollmentVerifySubmitPayloadError {
60+
#[error("Invalid payload serialization")]
61+
BadPayload,
62+
/// The signature couldn't be verified
63+
#[error("Invalid payload signature")]
64+
BadSignature,
65+
/// The signature is valid, but it has been done by someone unrelated to
66+
/// the requested email present in the payload.
67+
#[error("Requested email in the payload doesn't match the signature author's identity")]
68+
BadRequestedEmail,
69+
}
70+
71+
pub async fn async_enrollment_accept(
72+
cmds: &AuthenticatedCmds,
73+
event_bus: &EventBus,
74+
author: &LocalDevice,
75+
enrollment_id: AsyncEnrollmentID,
76+
profile: UserProfile,
77+
identity_strategy: &dyn AsyncEnrollmentAcceptIdentityStrategy,
78+
) -> Result<(), AsyncEnrollmentAcceptError> {
79+
// 1) Get back the enrollment submit payload from the server
80+
81+
let enrollment = {
82+
let enrollments = {
83+
use libparsec_client_connection::protocol::authenticated_cmds::latest::async_enrollment_list::{Req, Rep};
84+
85+
let rep = cmds.send(Req).await?;
86+
match rep {
87+
Rep::Ok { enrollments } => enrollments,
88+
Rep::AuthorNotAllowed => return Err(AsyncEnrollmentAcceptError::AuthorNotAllowed),
89+
bad_rep @ Rep::UnknownStatus { .. } => {
90+
return Err(anyhow::anyhow!("Unexpected server response: {:?}", bad_rep).into())
91+
}
92+
}
93+
};
94+
enrollments
95+
.into_iter()
96+
.find(|e| e.enrollment_id == enrollment_id)
97+
.ok_or(AsyncEnrollmentAcceptError::EnrollmentNotFound)?
98+
};
99+
100+
// 2) Validate the submit payload & the requested email vs identity consistency
101+
102+
let submit_payload = identity_strategy.verify_submit_payload(
103+
&enrollment.submit_payload,
104+
enrollment.submit_payload_signature,
105+
)?;
106+
107+
// 3) The submit payload is all good, now we can actually enroll the user
108+
109+
let mut timestamp = author.time_provider.now();
110+
loop {
111+
let (
112+
submitter_user_certificate,
113+
submitter_redacted_user_certificate,
114+
submitter_device_certificate,
115+
submitter_redacted_device_certificate,
116+
accept_payload,
117+
) = create_certificates_and_accept_payload(
118+
author,
119+
submit_payload.requested_device_label.clone(),
120+
submit_payload.requested_human_handle.clone(),
121+
profile,
122+
submit_payload.public_key.clone(),
123+
submit_payload.verify_key.clone(),
124+
timestamp,
125+
);
126+
let accept_payload_signature = identity_strategy.sign_accept_payload(&accept_payload);
127+
128+
{
129+
use libparsec_client_connection::protocol::authenticated_cmds::latest::async_enrollment_accept::{Req, Rep};
130+
131+
let rep = cmds
132+
.send(Req {
133+
enrollment_id,
134+
submitter_user_certificate,
135+
submitter_device_certificate,
136+
submitter_redacted_user_certificate,
137+
submitter_redacted_device_certificate,
138+
accept_payload,
139+
accept_payload_signature,
140+
})
141+
.await?;
142+
return match rep {
143+
Rep::Ok => Ok(()),
144+
Rep::RequireGreaterTimestamp {
145+
strictly_greater_than,
146+
} => {
147+
timestamp = greater_timestamp(
148+
&author.time_provider,
149+
GreaterTimestampOffset::User,
150+
strictly_greater_than,
151+
);
152+
continue;
153+
}
154+
Rep::AuthorNotAllowed => Err(AsyncEnrollmentAcceptError::AuthorNotAllowed),
155+
Rep::EnrollmentNotFound => Err(AsyncEnrollmentAcceptError::EnrollmentNotFound),
156+
Rep::EnrollmentNoLongerAvailable => Err(AsyncEnrollmentAcceptError::EnrollmentNoLongerAvailable),
157+
Rep::ActiveUsersLimitReached => Err(AsyncEnrollmentAcceptError::ActiveUsersLimitReached),
158+
Rep::HumanHandleAlreadyTaken => Err(AsyncEnrollmentAcceptError::HumanHandleAlreadyTaken),
159+
Rep::TimestampOutOfBallpark {
160+
server_timestamp,
161+
client_timestamp,
162+
ballpark_client_early_offset,
163+
ballpark_client_late_offset,
164+
} => {
165+
let event = EventTooMuchDriftWithServerClock {
166+
server_timestamp,
167+
ballpark_client_early_offset,
168+
ballpark_client_late_offset,
169+
client_timestamp,
170+
};
171+
event_bus.send(&event);
172+
173+
Err(AsyncEnrollmentAcceptError::TimestampOutOfBallpark {
174+
server_timestamp,
175+
client_timestamp,
176+
ballpark_client_early_offset,
177+
ballpark_client_late_offset,
178+
})
179+
}
180+
bad_rep @ (
181+
Rep::UserAlreadyExists // User & device IDs have just been randomly generated
182+
| Rep::InvalidCertificate
183+
| Rep::InvalidAcceptPayload
184+
| Rep::InvalidAcceptPayloadSignature
185+
| Rep::UnknownStatus { .. }
186+
) => {
187+
return Err(anyhow::anyhow!("Unexpected server response: {:?}", bad_rep).into())
188+
}
189+
};
190+
}
191+
}
192+
}
193+
194+
fn create_certificates_and_accept_payload(
195+
author: &LocalDevice,
196+
device_label: DeviceLabel,
197+
human_handle: HumanHandle,
198+
profile: UserProfile,
199+
public_key: PublicKey,
200+
verify_key: VerifyKey,
201+
timestamp: DateTime,
202+
) -> (Bytes, Bytes, Bytes, Bytes, Bytes) {
203+
let (
204+
user_id,
205+
device_id,
206+
user_certificate_bytes,
207+
redacted_user_certificate_bytes,
208+
device_certificate_bytes,
209+
redacted_device_certificate_bytes,
210+
) = create_user_and_device_certificates(
211+
author,
212+
device_label.clone(),
213+
human_handle.clone(),
214+
profile,
215+
public_key,
216+
verify_key,
217+
timestamp,
218+
);
219+
220+
let accept_payload = AsyncEnrollmentAcceptPayload {
221+
user_id,
222+
device_id,
223+
device_label,
224+
human_handle,
225+
profile,
226+
root_verify_key: author.root_verify_key().clone(),
227+
};
228+
let accept_payload_bytes = accept_payload.dump().into();
229+
230+
(
231+
user_certificate_bytes,
232+
redacted_user_certificate_bytes,
233+
device_certificate_bytes,
234+
redacted_device_certificate_bytes,
235+
accept_payload_bytes,
236+
)
237+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
2+
3+
use libparsec_client_connection::{AnonymousCmds, ConnectionError};
4+
use libparsec_types::prelude::*;
5+
6+
#[derive(Debug, thiserror::Error)]
7+
pub enum AsyncEnrollmentInfoError {
8+
#[error("Cannot communicate with the server: {0}")]
9+
Offline(#[from] ConnectionError),
10+
#[error("Enrollment not found")]
11+
EnrollmentNotFound,
12+
#[error(transparent)]
13+
Internal(#[from] anyhow::Error),
14+
}
15+
16+
#[derive(Debug, thiserror::Error)]
17+
pub enum AsyncEnrollmentVerifyAcceptPayloadError {
18+
#[error("Invalid payload serialization")]
19+
BadPayload,
20+
/// The signature couldn't be verified
21+
#[error("Invalid payload signature")]
22+
BadSignature,
23+
}
24+
25+
pub trait AsyncEnrollmentInfoIdentityStrategy {
26+
fn verify_accept_payload(
27+
&self,
28+
payload: &[u8],
29+
payload_signature: libparsec_client_connection::protocol::anonymous_cmds::latest::async_enrollment_info::AcceptPayloadSignature,
30+
) -> Result<AsyncEnrollmentSubmitPayload, AsyncEnrollmentVerifyAcceptPayloadError> {
31+
let submit_payload = AsyncEnrollmentSubmitPayload::load(payload)
32+
.map_err(|_| AsyncEnrollmentVerifyAcceptPayloadError::BadPayload)?;
33+
self._verify_accept_payload(payload, payload_signature)?;
34+
Ok(submit_payload)
35+
}
36+
fn _verify_accept_payload(
37+
&self,
38+
payload: &[u8],
39+
payload_signature: libparsec_client_connection::protocol::anonymous_cmds::latest::async_enrollment_info::AcceptPayloadSignature,
40+
) -> Result<AsyncEnrollmentSubmitPayload, AsyncEnrollmentVerifyAcceptPayloadError>;
41+
}
42+
43+
pub async fn async_enrollment_info(
44+
cmds: &AnonymousCmds,
45+
enrollment_id: AsyncEnrollmentID,
46+
) -> Result<(AsyncEnrollmentID, DateTime), AsyncEnrollmentInfoError> {
47+
let _status = {
48+
use libparsec_client_connection::protocol::anonymous_cmds::latest::async_enrollment_info::{Req, Rep};
49+
50+
let req = Req { enrollment_id };
51+
let rep = cmds.send(req).await?;
52+
match rep {
53+
Rep::Ok(status) => status,
54+
Rep::EnrollmentNotFound => return Err(AsyncEnrollmentInfoError::EnrollmentNotFound),
55+
bad_rep @ Rep::UnknownStatus { .. } => {
56+
return Err(anyhow::anyhow!("Unexpected server response: {:?}", bad_rep).into())
57+
}
58+
}
59+
};
60+
61+
todo!()
62+
63+
// {
64+
// use libparsec_protocol::anonymous_cmds::v5::async_enrollment_info::InfoStatus;
65+
// match status {
66+
// InfoStatus::Submitted { submitted_on } => Ok(Info::Submitted { submitted_on }),
67+
// InfoStatus::Rejected {
68+
// rejected_on,
69+
// submitted_on,
70+
// } => Ok(Info::Rejected {
71+
// rejected_on,
72+
// submitted_on,
73+
// }),
74+
// InfoStatus::Cancelled {
75+
// cancelled_on,
76+
// submitted_on,
77+
// } => Ok(Info::Cancelled {
78+
// cancelled_on,
79+
// submitted_on,
80+
// }),
81+
// InfoStatus::Accepted {
82+
// accepted_on,
83+
// accept_payload,
84+
// accept_payload_signature,
85+
// submitted_on,
86+
// } => Ok(Info::Accepted {
87+
// accepted_on,
88+
// accept_payload,
89+
// accept_payload_signature,
90+
// submitted_on,
91+
// }),
92+
// }
93+
// }
94+
}

0 commit comments

Comments
 (0)