Decouple client contracts and close private role registries
This commit is contained in:
parent
786049534d
commit
22325aa76a
5 changed files with 1041 additions and 321 deletions
|
|
@ -128,6 +128,20 @@ impl fmt::Debug for SandboxLayoutV1 {
|
|||
}
|
||||
|
||||
impl SandboxLayoutV1 {
|
||||
/// Returns the fixed protected destination set shared by all executable roles.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error only if the built-in layout violates the layout contract.
|
||||
pub fn default_layout() -> Result<Self, ConfigurationError> {
|
||||
Self::from_protected_destinations([
|
||||
"/workspace".to_owned(),
|
||||
"/run/lazaret".to_owned(),
|
||||
"/proc".to_owned(),
|
||||
"/sys".to_owned(),
|
||||
"/dev".to_owned(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Builds the closed destination set supplied by default sandbox-layout construction.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
mod names;
|
||||
mod session;
|
||||
mod signing;
|
||||
mod waiter;
|
||||
mod window;
|
||||
|
||||
pub use names::*;
|
||||
pub use session::*;
|
||||
pub use signing::*;
|
||||
pub use waiter::*;
|
||||
pub use window::*;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
|
||||
use crate::identity::{RequestId, SessionId};
|
||||
use crate::codec::{CodecError, encode};
|
||||
use crate::identity::{ClientId, RequestId, SessionId};
|
||||
use crate::records::GenerationIdentity;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn identifier_hex(bytes: &[u8]) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
|
@ -11,6 +14,21 @@ fn identifier_hex(bytes: &[u8]) -> String {
|
|||
})
|
||||
}
|
||||
|
||||
/// Returns the certificate subject binding for one generation-scoped Client identity.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error when the identity tuple cannot be encoded.
|
||||
pub fn client_enrollment_binding(
|
||||
generation_identity: &GenerationIdentity,
|
||||
cli_id: ClientId,
|
||||
) -> Result<String, CodecError> {
|
||||
let encoded = encode(&(generation_identity, cli_id))?;
|
||||
Ok(format!(
|
||||
"lazaret-client-{}",
|
||||
identifier_hex(&Sha256::digest(encoded))
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mailbox_generation_name(generation: u64) -> OsString {
|
||||
OsString::from(format!("generation-{generation:020}"))
|
||||
|
|
|
|||
123
crates/lazaret-core/src/protocol/waiter.rs
Normal file
123
crates/lazaret-core/src/protocol/waiter.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use std::fmt;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::codec;
|
||||
use crate::identity::{ClientId, ConfigurationFingerprint, DomainId, SessionAnchorId, SessionId};
|
||||
use crate::records::ProtocolVersion;
|
||||
|
||||
pub const BOOTSTRAP_WAITER_SOCKET_NAME: &str = "wait.sock";
|
||||
pub const PRIVATE_CLIENT_BOOTSTRAP_ROLE: &str = "--lazaret-private-client-bootstrap-v1";
|
||||
const MAX_WAITER_FRAME_BYTES: usize = 4_096;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct BootstrapWaitRequest {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub domain_id: DomainId,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
pub cli_id: ClientId,
|
||||
pub session_id: SessionId,
|
||||
pub session_anchor_id: SessionAnchorId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum BootstrapWaitOutcome {
|
||||
Waiting,
|
||||
Published,
|
||||
Failed,
|
||||
Refused,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BootstrapWaiterFrameError(String);
|
||||
|
||||
impl fmt::Display for BootstrapWaiterFrameError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BootstrapWaiterFrameError {}
|
||||
|
||||
/// Writes one bounded bootstrap-waiter frame.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses oversized or unencodable records and stream write failures.
|
||||
pub fn write_waiter_frame<T: Serialize>(
|
||||
stream: &mut impl Write,
|
||||
record: &T,
|
||||
) -> Result<(), BootstrapWaiterFrameError> {
|
||||
let encoded = codec::encode(record).map_err(failure)?;
|
||||
if encoded.len() > MAX_WAITER_FRAME_BYTES {
|
||||
return Err(failure("bootstrap waiter frame is oversized"));
|
||||
}
|
||||
let length = u32::try_from(encoded.len()).map_err(failure)?;
|
||||
stream.write_all(&length.to_be_bytes()).map_err(failure)?;
|
||||
stream.write_all(&encoded).map_err(failure)?;
|
||||
stream.flush().map_err(failure)
|
||||
}
|
||||
|
||||
/// Reads one bounded bootstrap-waiter frame.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses oversized, malformed, or incomplete frames and stream read failures.
|
||||
pub fn read_waiter_frame<T>(stream: &mut impl Read) -> Result<T, BootstrapWaiterFrameError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + Serialize,
|
||||
{
|
||||
let mut length = [0_u8; 4];
|
||||
stream.read_exact(&mut length).map_err(failure)?;
|
||||
let length = u32::from_be_bytes(length) as usize;
|
||||
if length > MAX_WAITER_FRAME_BYTES {
|
||||
return Err(failure("bootstrap waiter frame is oversized"));
|
||||
}
|
||||
let mut encoded = vec![0_u8; length];
|
||||
stream.read_exact(&mut encoded).map_err(failure)?;
|
||||
codec::decode(&encoded).map_err(failure)
|
||||
}
|
||||
|
||||
fn failure(error: impl fmt::Display) -> BootstrapWaiterFrameError {
|
||||
BootstrapWaiterFrameError(error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::identity::{ConfigurationFingerprint, DomainId};
|
||||
|
||||
#[test]
|
||||
fn waiter_frame_round_trips_and_rejects_oversize_length() {
|
||||
let outcome = BootstrapWaitOutcome::Published;
|
||||
let mut bytes = Vec::new();
|
||||
write_waiter_frame(&mut bytes, &outcome).unwrap();
|
||||
assert_eq!(
|
||||
read_waiter_frame::<BootstrapWaitOutcome>(&mut bytes.as_slice()).unwrap(),
|
||||
outcome
|
||||
);
|
||||
|
||||
let oversized_length = u32::try_from(MAX_WAITER_FRAME_BYTES + 1)
|
||||
.unwrap()
|
||||
.to_be_bytes();
|
||||
let mut oversized = oversized_length.as_slice();
|
||||
assert!(read_waiter_frame::<BootstrapWaitOutcome>(&mut oversized).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waiter_request_contract_keeps_identity_fields() {
|
||||
let request = BootstrapWaitRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
configuration_fingerprint: ConfigurationFingerprint::new([2; 32]),
|
||||
cli_id: ClientId::new([3; 32]),
|
||||
session_id: SessionId::new([4; 32]),
|
||||
session_anchor_id: SessionAnchorId::new([5; 32]),
|
||||
};
|
||||
let mut bytes = Vec::new();
|
||||
write_waiter_frame(&mut bytes, &request).unwrap();
|
||||
assert_eq!(
|
||||
read_waiter_frame::<BootstrapWaitRequest>(&mut bytes.as_slice()).unwrap(),
|
||||
request
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,12 @@ use super::{
|
|||
SessionAnchorId, SessionId, SourceIdentity, fmt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum StateResetScopeV1 {
|
||||
All,
|
||||
Selected(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct StoreMetadata {
|
||||
schema_id: String,
|
||||
|
|
|
|||
Loading…
Reference in a new issue