Add interactive client terminal transport
This commit is contained in:
parent
d597b05484
commit
6816f0632e
1 changed files with 1454 additions and 59 deletions
|
|
@ -9,16 +9,21 @@ use std::sync::Mutex;
|
|||
use ed25519_dalek::SigningKey;
|
||||
use lazaret_core::identity::{ClientId, SessionAnchorId, SessionId};
|
||||
use lazaret_core::records::{
|
||||
ClientEnrollmentResponse, ControlRequest, ControlResponse, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, ProtocolPublicationIdentity, ProtocolVersion, SessionBinding,
|
||||
SessionExitOutcome, SessionRegistrationRequest, TlsSubjectId,
|
||||
ClientEnrollmentResponse, ControlRequest, ControlResponse, DeveloperDecision,
|
||||
DeveloperDecisionKind, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionRequest, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
||||
ProtocolPublicationIdentity, ProtocolVersion, SessionBinding, SessionExitOutcome,
|
||||
SessionRegistrationRequest, TlsSubjectId,
|
||||
};
|
||||
use lazaret_host_coordinator::control::{
|
||||
ControlError, PendingClientEnrollment, PublishedControlFace,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::terminal::{HostTerminal, InteractiveSessionTransport, run_interactive_session};
|
||||
|
||||
pub mod production;
|
||||
pub mod terminal;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClientCommand {
|
||||
|
|
@ -300,6 +305,45 @@ impl ClientSessionIdentity {
|
|||
pub fn session_anchor_id(&self) -> SessionAnchorId {
|
||||
SessionAnchorId::new(Sha256::digest(self.anchor.verifying_key().as_bytes()).into())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn session_anchor_public_key(&self) -> [u8; 32] {
|
||||
self.anchor.verifying_key().to_bytes()
|
||||
}
|
||||
|
||||
/// Signs one exact prompt decision with this Client Session's host-only Session Anchor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns a protocol encoding failure.
|
||||
pub fn sign_developer_decision<E>(
|
||||
&self,
|
||||
session_binding: SessionBinding,
|
||||
prompt: &lazaret_core::records::TerminalPrompt,
|
||||
decision: DeveloperDecisionKind,
|
||||
) -> Result<Vec<u8>, ClientError<E>> {
|
||||
if session_binding.session_id != self.session_id
|
||||
|| session_binding.cli_id != self.cli_id
|
||||
|| prompt.session_id != self.session_id
|
||||
{
|
||||
return Err(ClientError::Protocol("prompt identity differs"));
|
||||
}
|
||||
lazaret_core::protocol::encode_signed(
|
||||
&DeveloperDecision {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding,
|
||||
request_id: prompt.request_id,
|
||||
request_sha256: prompt.request_sha256,
|
||||
decision,
|
||||
approved_command_argv: if decision == DeveloperDecisionKind::Approve {
|
||||
prompt.command_argv.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
},
|
||||
&self.anchor,
|
||||
)
|
||||
.map_err(|_| ClientError::Protocol("developer decision encoding failed"))
|
||||
}
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
|
|
@ -425,6 +469,137 @@ pub fn execute_over_control_face_as<E>(
|
|||
}
|
||||
}
|
||||
|
||||
/// Executes one interactive Session through the exact identity that owns its Session Anchor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns enrollment, registration, terminal, framing, or runtime-loss failures.
|
||||
pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
||||
endpoint: &ControlEndpoint,
|
||||
argv: &[Vec<u8>],
|
||||
terminal: &mut H,
|
||||
identity: &ClientSessionIdentity,
|
||||
) -> Result<i32, ClientError<E>> {
|
||||
if argv.is_empty() {
|
||||
return Err(ClientError::EmptyCommand);
|
||||
}
|
||||
let generation = endpoint.publication.generation_identity.clone();
|
||||
let cli_id = identity.cli_id;
|
||||
let session_id = identity.session_id;
|
||||
let pending = PendingClientEnrollment::create(generation.clone(), cli_id)?;
|
||||
let anonymous = PublishedControlFace::client_config(&endpoint.publication, None)?;
|
||||
let mut enrollment =
|
||||
PublishedControlFace::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
|
||||
PublishedControlFace::send(
|
||||
&mut enrollment,
|
||||
&ControlRequest::Enroll(pending.request().clone()),
|
||||
)?;
|
||||
let ControlResponse::Enrolled(ClientEnrollmentResponse { credential, .. }) =
|
||||
PublishedControlFace::receive(&mut enrollment)?
|
||||
else {
|
||||
return Err(ClientError::Protocol(
|
||||
"enrollment response was not Enrolled",
|
||||
));
|
||||
};
|
||||
if credential.generation_identity != generation
|
||||
|| credential.subject_id() != TlsSubjectId::ClientCli(cli_id)
|
||||
{
|
||||
return Err(ClientError::Protocol(
|
||||
"enrollment credential identity differs",
|
||||
));
|
||||
}
|
||||
drop(enrollment);
|
||||
let binding = SessionBinding {
|
||||
generation_identity: generation,
|
||||
cli_id,
|
||||
session_id,
|
||||
session_anchor_public_key: identity.anchor.verifying_key().to_bytes().to_vec(),
|
||||
};
|
||||
let authenticated = PublishedControlFace::client_config(
|
||||
&endpoint.publication,
|
||||
Some((&credential, pending.private_key())),
|
||||
)?;
|
||||
let mut stream =
|
||||
PublishedControlFace::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
|
||||
PublishedControlFace::send(
|
||||
&mut stream,
|
||||
&ControlRequest::Register(SessionRegistrationRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: binding.clone(),
|
||||
}),
|
||||
)?;
|
||||
let ControlResponse::Registered(registered) = PublishedControlFace::receive(&mut stream)?
|
||||
else {
|
||||
return Err(ClientError::Protocol(
|
||||
"registration response was not Registered",
|
||||
));
|
||||
};
|
||||
if registered.session_capability.session_binding != binding {
|
||||
return Err(ClientError::Protocol(
|
||||
"registration capability identity differs",
|
||||
));
|
||||
}
|
||||
endpoint.acknowledge_waiter_transfer()?;
|
||||
let mut transport = ControlFaceInteractiveTransport { stream };
|
||||
run_interactive_session(&mut transport, terminal, identity, &binding, argv)
|
||||
.map_err(widen_client_error)
|
||||
}
|
||||
|
||||
struct ControlFaceInteractiveTransport {
|
||||
stream: rustls::StreamOwned<rustls::ClientConnection, std::os::unix::net::UnixStream>,
|
||||
}
|
||||
|
||||
impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn start(
|
||||
&mut self,
|
||||
request: InteractiveSessionRequest,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
PublishedControlFace::send(
|
||||
&mut self.stream,
|
||||
&ControlRequest::ExecuteInteractive(request),
|
||||
)?;
|
||||
receive_interactive_batch(&mut self.stream)
|
||||
}
|
||||
|
||||
fn exchange(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
PublishedControlFace::send(&mut self.stream, &event).map_err(map_control_connection)?;
|
||||
receive_interactive_batch(&mut self.stream)
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_interactive_batch(
|
||||
stream: &mut impl std::io::Read,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<std::convert::Infallible>> {
|
||||
PublishedControlFace::receive(stream).map_err(map_control_connection)
|
||||
}
|
||||
|
||||
fn map_control_connection(error: ControlError) -> ClientError<std::convert::Infallible> {
|
||||
match error {
|
||||
ControlError::Io(_) => ClientError::LostConnection,
|
||||
other => ClientError::Control(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn widen_client_error<E>(error: ClientError<std::convert::Infallible>) -> ClientError<E> {
|
||||
match error {
|
||||
ClientError::Configuration(message) => ClientError::Configuration(message),
|
||||
ClientError::UnsupportedHost(message) => ClientError::UnsupportedHost(message),
|
||||
ClientError::StaleCoordinator => ClientError::StaleCoordinator,
|
||||
ClientError::LostConnection => ClientError::LostConnection,
|
||||
ClientError::BootstrapOwnerDisconnected => ClientError::BootstrapOwnerDisconnected,
|
||||
ClientError::WaitingDomainFailed => ClientError::WaitingDomainFailed,
|
||||
ClientError::EmptyCommand => ClientError::EmptyCommand,
|
||||
ClientError::Backend(error) => match error {},
|
||||
ClientError::Io(error) => ClientError::Io(error),
|
||||
ClientError::Control(error) => ClientError::Control(error),
|
||||
ClientError::Protocol(message) => ClientError::Protocol(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn random_bytes<E>() -> Result<[u8; 32], ClientError<E>> {
|
||||
let mut bytes = [0_u8; 32];
|
||||
getrandom::fill(&mut bytes)
|
||||
|
|
|
|||
Loading…
Reference in a new issue