Decouple client contracts and close private role registries

This commit is contained in:
ms2k 2026-07-19 03:44:30 +09:00
commit 22325aa76a
5 changed files with 1041 additions and 321 deletions

View file

@ -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

View file

@ -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::*;

View file

@ -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}"))

View 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
);
}
}

View file

@ -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,