lazaret/crates/lazaret-host-coordinator/src/control.rs

1239 lines
46 KiB
Rust
Raw Normal View History

use std::collections::BTreeMap;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::fd::AsFd;
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard};
use ed25519_dalek::SigningKey;
use lazaret_core::codec::{CodecError, decode, encode};
use lazaret_core::identity::{
CertificateDigest, ClientId, SessionAnchorId, SessionId, TrustManifestDigest,
};
use lazaret_core::protocol::{
ProtocolValidationError, issue_protocol_credential, issue_session_capability,
issue_trust_manifest, verify_tls_credential,
};
use lazaret_core::records::{
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
DurableDecision, GenerationIdentity, ProtocolCredential, ProtocolCredentialRole,
ProtocolPublicationIdentity, ProtocolSubjectId, ProtocolVersion, SandboxCredentialBundle,
Session, SessionBinding, SessionPhase, SessionRegistrationRequest, SessionRegistrationResponse,
TlsCredential, TlsRole, TlsSubjectId, TrustManifest,
};
use rcgen::{
BasicConstraints, CertificateParams, CertificateSigningRequestParams, CertifiedIssuer,
DistinguishedName, DnType, DnValue, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
PKCS_ED25519,
};
use rustix::fs::{Mode, OFlags, openat};
use rustix::net::sockopt::socket_peercred;
use rustls::pki_types::{
CertificateDer, CertificateSigningRequestDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName,
};
use rustls::server::{NoServerSessionStorage, WebPkiClientVerifier};
use rustls::{ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection};
use sha2::{Digest, Sha256};
use time::{Duration, OffsetDateTime};
use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinatorDomainStore};
pub const CONTROL_ALPN: &[u8] = b"lazaret-control/1";
pub const CONTROL_SERVER_NAME: &str = "coordinator.lazaret.invalid";
pub const MAX_ENROLLMENT_BYTES: usize = 8_192;
const MAX_CONTROL_BYTES: usize = 65_536;
const CONTROL_SOCKET_NAME: &str = "control.sock";
const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
#[derive(Debug)]
pub enum ControlError {
Io(io::Error),
Codec(CodecError),
Certificate(rcgen::Error),
Tls(rustls::Error),
Protocol(ProtocolValidationError),
DomainStore(DomainStoreError),
InvalidPath(&'static str),
InvalidPublication(&'static str),
UnauthorizedPeer,
InvalidCertificateProfile,
StaleGeneration,
DuplicateIdentity,
InvalidRequest,
OversizeRequest { length: usize, maximum: usize },
LockPoisoned,
}
impl fmt::Display for ControlError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "control I/O failed: {error}"),
Self::Codec(error) => write!(formatter, "control record failed validation: {error}"),
Self::Certificate(error) => write!(formatter, "certificate operation failed: {error}"),
Self::Tls(error) => write!(formatter, "TLS failed: {error}"),
Self::Protocol(error) => write!(formatter, "protocol validation failed: {error}"),
Self::DomainStore(error) => write!(formatter, "session persistence failed: {error}"),
Self::InvalidPath(reason) => write!(formatter, "invalid control path: {reason}"),
Self::InvalidPublication(reason) => {
write!(formatter, "invalid control publication: {reason}")
}
Self::UnauthorizedPeer => formatter.write_str("control peer UID is not authorized"),
Self::InvalidCertificateProfile => {
formatter.write_str("TLS certificate profile is invalid")
}
Self::StaleGeneration => formatter.write_str("control generation is stale"),
Self::DuplicateIdentity => formatter.write_str("session identity is already active"),
Self::InvalidRequest => {
formatter.write_str("operation is not allowed on this connection")
}
Self::OversizeRequest { length, maximum } => {
write!(
formatter,
"control request length {length} exceeds {maximum}"
)
}
Self::LockPoisoned => formatter.write_str("control registry lock is poisoned"),
}
}
}
impl std::error::Error for ControlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Codec(error) => Some(error),
Self::Certificate(error) => Some(error),
Self::Tls(error) => Some(error),
Self::Protocol(error) => Some(error),
Self::DomainStore(error) => Some(error),
_ => None,
}
}
}
macro_rules! from_error {
($source:ty, $variant:ident) => {
impl From<$source> for ControlError {
fn from(error: $source) -> Self {
Self::$variant(error)
}
}
};
}
from_error!(io::Error, Io);
from_error!(CodecError, Codec);
from_error!(rcgen::Error, Certificate);
from_error!(rustls::Error, Tls);
from_error!(ProtocolValidationError, Protocol);
from_error!(DomainStoreError, DomainStore);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct FileIdentity {
device: u64,
inode: u64,
}
impl FileIdentity {
fn from_metadata(metadata: &std::fs::Metadata) -> Self {
Self {
device: metadata.dev(),
inode: metadata.ino(),
}
}
}
pub struct PendingClientEnrollment {
request: ClientEnrollmentRequest,
private_key: PrivatePkcs8KeyDer<'static>,
}
impl PendingClientEnrollment {
/// Creates an Ed25519 CSR whose signed subject binds the exact generation and Client CLI ID.
///
/// # Errors
/// Returns an error when key generation, encoding, or CSR signing fails.
pub fn create(
generation_identity: GenerationIdentity,
cli_id: ClientId,
) -> Result<Self, ControlError> {
let binding = enrollment_binding(&generation_identity, cli_id)?;
let mut params = CertificateParams::default();
params.distinguished_name = DistinguishedName::new();
params.distinguished_name.push(DnType::CommonName, binding);
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
let key = KeyPair::generate_for(&PKCS_ED25519)?;
let csr = params.serialize_request(&key)?;
Ok(Self {
request: ClientEnrollmentRequest {
protocol_version: ProtocolVersion::V1,
generation_identity,
cli_id,
certificate_signing_request_der: csr.der().to_vec(),
},
private_key: PrivatePkcs8KeyDer::from(key.serialize_der()),
})
}
#[must_use]
pub const fn request(&self) -> &ClientEnrollmentRequest {
&self.request
}
#[must_use]
pub fn private_key(&self) -> PrivateKeyDer<'static> {
PrivateKeyDer::Pkcs8(self.private_key.clone_key())
}
}
pub struct TrustAnchor {
generation_identity: GenerationIdentity,
tls_ca: CertifiedIssuer<'static, KeyPair>,
tls_ca_der: CertificateDer<'static>,
coordinator_tls_credential: TlsCredential,
coordinator_tls_private_key: PrivatePkcs8KeyDer<'static>,
trust_manifest: TrustManifest,
trust_manifest_digest: TrustManifestDigest,
coordinator_protocol_credential: ProtocolCredential,
sandbox_protocol_credential: ProtocolCredential,
coordinator_protocol_key: SigningKey,
sandbox_protocol_key: Option<SigningKey>,
}
impl TrustAnchor {
/// Creates one ephemeral generation Trust Anchor with role-separated Ed25519 keys.
///
/// # Errors
/// Returns an error when key generation, certificate issuance, or protocol signing fails.
pub fn create(generation_identity: GenerationIdentity) -> Result<Self, ControlError> {
let now = OffsetDateTime::now_utc();
let mut ca_params = CertificateParams::default();
ca_params.distinguished_name = DistinguishedName::new();
ca_params
.distinguished_name
.push(DnType::CommonName, "Lazaret ephemeral TLS CA");
ca_params.not_before = now - Duration::minutes(1);
ca_params.not_after = now + Duration::hours(24);
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
];
let ca_key = KeyPair::generate_for(&PKCS_ED25519)?;
let tls_ca = CertifiedIssuer::self_signed(ca_params, ca_key)?;
let tls_ca_der = tls_ca.der().clone();
let tls_ca_digest = certificate_digest(&tls_ca_der);
let protocol_issuer = random_signing_key()?;
let (trust_manifest, trust_manifest_digest) =
issue_trust_manifest(generation_identity.clone(), tls_ca_digest, &protocol_issuer)?;
let coordinator_protocol_key = random_signing_key()?;
let sandbox_protocol_key = random_signing_key()?;
let coordinator_protocol_credential = issue_protocol_credential(
generation_identity.clone(),
ProtocolCredentialRole::SessionCoordinator,
ProtocolSubjectId::SessionCoordinator(generation_identity.coordinator_id),
&coordinator_protocol_key.verifying_key(),
&protocol_issuer,
)?;
let sandbox_protocol_credential = issue_protocol_credential(
generation_identity.clone(),
ProtocolCredentialRole::AgentSandbox,
ProtocolSubjectId::AgentSandbox(generation_identity.sandbox_id),
&sandbox_protocol_key.verifying_key(),
&protocol_issuer,
)?;
let coordinator_key = KeyPair::generate_for(&PKCS_ED25519)?;
let coordinator_certificate = leaf_parameters(
CONTROL_SERVER_NAME,
ExtendedKeyUsagePurpose::ServerAuth,
now,
)?
.signed_by(&coordinator_key, &tls_ca)?;
let coordinator_certificate_der = coordinator_certificate.der().to_vec();
let coordinator_tls_credential = TlsCredential::session_coordinator(
generation_identity.clone(),
generation_identity.coordinator_id,
coordinator_certificate_der.clone(),
certificate_digest(coordinator_certificate.der()),
);
Ok(Self {
generation_identity,
tls_ca,
tls_ca_der,
coordinator_tls_credential,
coordinator_tls_private_key: PrivatePkcs8KeyDer::from(coordinator_key.serialize_der()),
trust_manifest,
trust_manifest_digest,
coordinator_protocol_credential,
sandbox_protocol_credential,
coordinator_protocol_key,
sandbox_protocol_key: Some(sandbox_protocol_key),
})
}
#[must_use]
pub fn publication_identity(&self) -> ProtocolPublicationIdentity {
ProtocolPublicationIdentity {
protocol_version: ProtocolVersion::V1,
generation_identity: self.generation_identity.clone(),
tls_ca_certificate_der: self.tls_ca_der.to_vec(),
trust_manifest_sha256: self.trust_manifest_digest,
coordinator_tls_certificate_sha256: self.coordinator_tls_credential.certificate_sha256,
}
}
/// Transfers the sandbox credential package once without exposing coordinator or issuer keys.
///
/// # Errors
/// Returns an error after the sandbox private key has already been transferred.
pub fn take_sandbox_credential_bundle(
&mut self,
) -> Result<SandboxCredentialBundle, ControlError> {
let private_key = self
.sandbox_protocol_key
.take()
.ok_or(ControlError::InvalidRequest)?
.to_bytes()
.to_vec();
SandboxCredentialBundle::new(
self.generation_identity.clone(),
self.trust_manifest.clone(),
self.coordinator_protocol_credential.clone(),
self.sandbox_protocol_credential.clone(),
private_key,
)
.map_err(|_| ControlError::InvalidCertificateProfile)
}
fn issue_client_credential(
&self,
request: &ClientEnrollmentRequest,
) -> Result<TlsCredential, ControlError> {
if request.protocol_version != ProtocolVersion::V1
|| request.generation_identity != self.generation_identity
{
return Err(ControlError::StaleGeneration);
}
let csr_der =
CertificateSigningRequestDer::from(request.certificate_signing_request_der.as_slice());
let csr = CertificateSigningRequestParams::from_der(&csr_der)?;
if csr.public_key.algorithm() != &PKCS_ED25519
|| csr.params.distinguished_name.get(&DnType::CommonName)
!= Some(&DnValue::Utf8String(enrollment_binding(
&request.generation_identity,
request.cli_id,
)?))
{
return Err(ControlError::InvalidCertificateProfile);
}
let now = OffsetDateTime::now_utc();
let params = leaf_parameters(
&format!("client-{}", short_identity(request.cli_id.as_bytes())),
ExtendedKeyUsagePurpose::ClientAuth,
now,
)?;
let constrained = CertificateSigningRequestParams {
params,
public_key: csr.public_key,
};
let certificate = constrained.signed_by(&self.tls_ca)?;
let certificate_der = certificate.der().to_vec();
Ok(TlsCredential::client_cli(
self.generation_identity.clone(),
request.cli_id,
certificate_der,
certificate_digest(certificate.der()),
))
}
fn server_config(&self) -> Result<ServerConfig, ControlError> {
let mut roots = RootCertStore::empty();
roots.add(self.tls_ca_der.clone())?;
let verifier = WebPkiClientVerifier::builder(Arc::new(roots))
.allow_unauthenticated()
.build()
.map_err(|_| ControlError::InvalidCertificateProfile)?;
let mut config = ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_client_cert_verifier(verifier)
.with_single_cert(
vec![CertificateDer::from(
self.coordinator_tls_credential.certificate_der.clone(),
)],
PrivateKeyDer::Pkcs8(self.coordinator_tls_private_key.clone_key()),
)?;
config.alpn_protocols = vec![CONTROL_ALPN.to_vec()];
config.max_early_data_size = 0;
config.send_tls13_tickets = 0;
config.session_storage = Arc::new(NoServerSessionStorage {});
Ok(config)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RegisteredSession {
pub binding: SessionBinding,
pub session_anchor_id: SessionAnchorId,
}
#[derive(Default)]
struct RegistryState {
enrolled: BTreeMap<CertificateDigest, TlsCredential>,
connected: BTreeMap<SessionId, RegisteredSession>,
}
pub struct SessionRegistry {
state: Mutex<RegistryState>,
store: Arc<HostCoordinatorDomainStore>,
generation_identity: GenerationIdentity,
}
impl SessionRegistry {
fn new(
store: Arc<HostCoordinatorDomainStore>,
generation_identity: GenerationIdentity,
) -> Self {
Self {
state: Mutex::new(RegistryState::default()),
store,
generation_identity,
}
}
/// Returns the exact set of connected, registered sessions.
///
/// # Errors
/// Returns an error if the registry lock is poisoned.
pub fn connected_sessions(&self) -> Result<Vec<RegisteredSession>, ControlError> {
Ok(self.lock()?.connected.values().cloned().collect())
}
fn lock(&self) -> Result<MutexGuard<'_, RegistryState>, ControlError> {
self.state.lock().map_err(|_| ControlError::LockPoisoned)
}
fn enroll(&self, credential: TlsCredential) -> Result<(), ControlError> {
self.lock()?
.enrolled
.insert(credential.certificate_sha256, credential);
Ok(())
}
fn credential_for_leaf(&self, certificate: &[u8]) -> Result<TlsCredential, ControlError> {
self.lock()?
.enrolled
.get(&certificate_digest(certificate))
.cloned()
.ok_or(ControlError::InvalidCertificateProfile)
}
fn register(
&self,
request: SessionRegistrationRequest,
credential: &TlsCredential,
) -> Result<RegisteredSession, ControlError> {
let binding = request.session_binding;
if request.protocol_version != ProtocolVersion::V1
|| binding.generation_identity != self.generation_identity
{
return Err(ControlError::StaleGeneration);
}
let TlsSubjectId::ClientCli(certificate_cli_id) = credential.subject_id() else {
return Err(ControlError::InvalidCertificateProfile);
};
if credential.role() != TlsRole::ClientCli || certificate_cli_id != binding.cli_id {
return Err(ControlError::InvalidCertificateProfile);
}
verify_tls_credential(
credential,
&self.generation_identity,
TlsRole::ClientCli,
TlsSubjectId::ClientCli(binding.cli_id),
)?;
let anchor_bytes: [u8; 32] = binding
.session_anchor_public_key
.as_slice()
.try_into()
.map_err(|_| ControlError::InvalidRequest)?;
ed25519_dalek::VerifyingKey::from_bytes(&anchor_bytes)
.map_err(|_| ControlError::InvalidRequest)?;
let session_anchor_id = SessionAnchorId::new(sha256(&anchor_bytes));
self.require_serviceable_generation()?;
let snapshot = self.store.snapshot()?;
let state = self.lock()?;
let duplicate_in_memory = state.connected.values().any(|registered| {
registered.binding.session_id == binding.session_id
|| registered.binding.cli_id == binding.cli_id
|| registered.session_anchor_id == session_anchor_id
});
let duplicate_session = snapshot.sessions.iter().any(|session| {
session.session_id == binding.session_id
|| (is_live_session(session.session_phase)
&& (session.cli_id == binding.cli_id
|| session.session_anchor_id == session_anchor_id))
});
if duplicate_in_memory || duplicate_session {
return Err(ControlError::DuplicateIdentity);
}
let registering = Session {
generation_id: binding.generation_identity.generation_id,
session_id: binding.session_id,
cli_id: binding.cli_id,
session_anchor_id,
session_phase: SessionPhase::Registering,
};
self.store
.apply(&[DomainStoreMutation::PutSession(registering.clone())])?;
let mut attached = registering;
attached.session_phase = SessionPhase::Attached;
if let Err(error) = self
.store
.apply(&[DomainStoreMutation::PutSession(attached)])
{
let mut failed = Session {
generation_id: binding.generation_identity.generation_id,
session_id: binding.session_id,
cli_id: binding.cli_id,
session_anchor_id,
session_phase: SessionPhase::Registering,
};
failed.session_phase = SessionPhase::Failed;
let _ = self.store.apply(&[DomainStoreMutation::PutSession(failed)]);
return Err(error.into());
}
let registered = RegisteredSession {
binding,
session_anchor_id,
};
drop(snapshot);
drop(state);
self.lock()?
.connected
.insert(registered.binding.session_id, registered.clone());
Ok(registered)
}
fn disconnect(&self, session_id: SessionId) -> Result<(), ControlError> {
let registered = self.lock()?.connected.remove(&session_id);
let Some(registered) = registered else {
return Ok(());
};
self.store
.apply(&[DomainStoreMutation::PutSession(Session {
generation_id: registered.binding.generation_identity.generation_id,
session_id,
cli_id: registered.binding.cli_id,
session_anchor_id: registered.session_anchor_id,
session_phase: SessionPhase::Closed,
})])?;
Ok(())
}
fn require_serviceable_generation(&self) -> Result<(), ControlError> {
let decision = self.require_generation_authority()?;
if decision != DurableDecision::Published {
return Err(ControlError::StaleGeneration);
}
Ok(())
}
fn require_generation_authority(&self) -> Result<DurableDecision, ControlError> {
let snapshot = self.store.snapshot()?;
let generation = snapshot
.generations
.iter()
.find(|generation| generation.generation_id == self.generation_identity.generation_id)
.ok_or(ControlError::StaleGeneration)?;
let is_latest = snapshot
.generations
.iter()
.all(|candidate| candidate.generation_id <= self.generation_identity.generation_id);
if !is_latest
|| snapshot.domain.domain_id != self.generation_identity.domain_id
|| snapshot.domain.profile_name != self.generation_identity.agent_profile
|| generation.domain_id != self.generation_identity.domain_id
|| generation.configuration_fingerprint
!= self.generation_identity.configuration_fingerprint
|| generation.coordinator_id != self.generation_identity.coordinator_id
|| generation.sandbox_id != self.generation_identity.sandbox_id
|| !matches!(
generation.durable_decision,
DurableDecision::Preparing | DurableDecision::Published
)
{
return Err(ControlError::StaleGeneration);
}
Ok(generation.durable_decision)
}
}
struct ControlPaths {
root: PathBuf,
root_file: File,
root_identity: FileIdentity,
socket: PathBuf,
identity: PathBuf,
}
impl ControlPaths {
fn open(root: &Path, uid: u32) -> Result<Self, ControlError> {
if !root.is_absolute() {
return Err(ControlError::InvalidPath(
"runtime Domain root is not absolute",
));
}
let canonical = std::fs::canonicalize(root)?;
if canonical != root {
return Err(ControlError::InvalidPath(
"runtime Domain root contains a symlink",
));
}
let metadata = std::fs::symlink_metadata(root)?;
if !metadata.file_type().is_dir()
|| metadata.uid() != uid
|| metadata.mode() & 0o7777 != 0o700
{
return Err(ControlError::InvalidPath(
"runtime Domain root is not invoking-user-owned mode 0700",
));
}
let root_file = OpenOptions::new().read(true).open(root)?;
let root_identity = FileIdentity::from_metadata(&root_file.metadata()?);
Ok(Self {
root: root.to_path_buf(),
root_file,
root_identity,
socket: root.join(CONTROL_SOCKET_NAME),
identity: root.join(PUBLICATION_IDENTITY_NAME),
})
}
fn validate_root(&self, uid: u32) -> Result<(), ControlError> {
let descriptor = self.root_file.metadata()?;
let current = std::fs::symlink_metadata(&self.root)?;
if FileIdentity::from_metadata(&descriptor) != self.root_identity
|| FileIdentity::from_metadata(&current) != self.root_identity
|| !current.file_type().is_dir()
|| current.uid() != uid
|| current.mode() & 0o7777 != 0o700
{
return Err(ControlError::InvalidPath("runtime Domain root changed"));
}
Ok(())
}
fn create_identity(&self, bytes: &[u8]) -> Result<(File, FileIdentity), ControlError> {
self.validate_root(self.root_file.metadata()?.uid())?;
let fd = openat(
self.root_file.as_fd(),
PUBLICATION_IDENTITY_NAME,
OFlags::CREATE | OFlags::EXCL | OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::RUSR | Mode::WUSR,
)
.map_err(io::Error::from)?;
let mut file = File::from(fd);
file.write_all(bytes)
.map_err(|_| ControlError::InvalidPublication("identity write failed"))?;
file.sync_all()
.map_err(|_| ControlError::InvalidPublication("identity sync failed"))?;
self.root_file
.sync_all()
.map_err(|_| ControlError::InvalidPublication("directory sync failed"))?;
let metadata = file.metadata()?;
Ok((file, FileIdentity::from_metadata(&metadata)))
}
}
pub struct PublishedControlFace {
listener: std::os::unix::net::UnixListener,
paths: ControlPaths,
socket_identity: FileIdentity,
identity_file: File,
identity_file_identity: FileIdentity,
publication_identity: ProtocolPublicationIdentity,
publication_bytes: Vec<u8>,
uid: u32,
anchor: TrustAnchor,
server_config: Arc<ServerConfig>,
registry: Arc<SessionRegistry>,
}
impl PublishedControlFace {
/// Publishes one generation control face in the fixed host-private runtime path.
///
/// # Errors
/// Refuses stale generations, existing publication objects, path/ownership/mode errors, and
/// TLS configuration failures.
pub fn publish(
runtime_domain_root: &Path,
uid: u32,
anchor: TrustAnchor,
store: Arc<HostCoordinatorDomainStore>,
) -> Result<Self, ControlError> {
let paths = ControlPaths::open(runtime_domain_root, uid)?;
if paths.socket.try_exists()? || paths.identity.try_exists()? {
return Err(ControlError::InvalidPublication(
"publication path already exists",
));
}
let registry = Arc::new(SessionRegistry::new(
store,
anchor.generation_identity.clone(),
));
registry.require_generation_authority()?;
let publication_identity = anchor.publication_identity();
let publication_bytes = encode(&publication_identity)?;
let server_config = Arc::new(anchor.server_config()?);
let (identity_file, identity_file_identity) = paths.create_identity(&publication_bytes)?;
let listener = match std::os::unix::net::UnixListener::bind(&paths.socket) {
Ok(listener) => listener,
Err(error) => {
let _ = std::fs::remove_file(&paths.identity);
return Err(ControlError::InvalidPublication(
if error.kind() == io::ErrorKind::PermissionDenied {
"control socket bind was denied"
} else {
"control socket bind failed"
},
));
}
};
let finalize = (|| {
std::fs::set_permissions(&paths.socket, std::fs::Permissions::from_mode(0o600))
.map_err(|_| ControlError::InvalidPublication("control socket chmod failed"))?;
let socket_metadata = std::fs::symlink_metadata(&paths.socket)?;
if !socket_metadata.file_type().is_socket()
|| socket_metadata.uid() != uid
|| socket_metadata.mode() & 0o7777 != 0o600
{
return Err(ControlError::InvalidPublication(
"control socket ownership or mode is invalid",
));
}
paths.validate_root(uid)?;
Ok(FileIdentity::from_metadata(&socket_metadata))
})();
let socket_identity = match finalize {
Ok(identity) => identity,
Err(error) => {
drop(listener);
let _ = std::fs::remove_file(&paths.socket);
let _ = std::fs::remove_file(&paths.identity);
return Err(error);
}
};
let identity_unchanged = identity_file
.metadata()
.map(|metadata| FileIdentity::from_metadata(&metadata) == identity_file_identity);
match identity_unchanged {
Ok(true) => {}
Ok(false) => {
drop(listener);
let _ = std::fs::remove_file(&paths.socket);
let _ = std::fs::remove_file(&paths.identity);
return Err(ControlError::InvalidPublication(
"publication identity changed during creation",
));
}
Err(error) => {
drop(listener);
let _ = std::fs::remove_file(&paths.socket);
let _ = std::fs::remove_file(&paths.identity);
return Err(error.into());
}
}
Ok(Self {
listener,
paths,
socket_identity,
identity_file,
identity_file_identity,
publication_identity,
publication_bytes,
uid,
anchor,
server_config,
registry,
})
}
#[must_use]
pub fn publication_identity(&self) -> &ProtocolPublicationIdentity {
&self.publication_identity
}
#[must_use]
pub fn registry(&self) -> Arc<SessionRegistry> {
Arc::clone(&self.registry)
}
#[must_use]
pub fn socket_path(&self) -> &Path {
&self.paths.socket
}
#[must_use]
pub fn identity_path(&self) -> &Path {
&self.paths.identity
}
/// Accepts and serves one TLS connection. Registered connections remain live until EOF.
///
/// # Errors
/// Refuses peer UID, publication, TLS, ALPN, certificate, framing, tuple, duplicate, and
/// persistence failures.
pub fn serve_next(&self) -> Result<(), ControlError> {
let (socket, _) = self.listener.accept()?;
let peer = socket_peercred(&socket).map_err(io::Error::from)?;
require_peer_uid(peer.uid.as_raw(), self.uid)?;
self.validate_publication()?;
let mut connection = ServerConnection::new(Arc::clone(&self.server_config))?;
let mut socket = socket;
while connection.is_handshaking() {
connection.complete_io(&mut socket)?;
}
if connection.alpn_protocol() != Some(CONTROL_ALPN) {
return Err(ControlError::InvalidRequest);
}
let peer_certificate = connection.peer_certificates().map(|certificates| {
certificates
.iter()
.map(|certificate| certificate.to_vec())
.collect::<Vec<_>>()
});
let mut stream = rustls::StreamOwned::new(connection, socket);
match peer_certificate {
None => self.serve_enrollment(&mut stream),
Some(certificates) => self.serve_registration(&mut stream, &certificates),
}
}
fn serve_enrollment(
&self,
stream: &mut rustls::StreamOwned<ServerConnection, std::os::unix::net::UnixStream>,
) -> Result<(), ControlError> {
let request: ControlRequest = read_frame(stream, MAX_ENROLLMENT_BYTES)?;
let ControlRequest::Enroll(request) = request else {
return Err(ControlError::InvalidRequest);
};
let credential = self.anchor.issue_client_credential(&request)?;
self.registry.enroll(credential.clone())?;
write_frame(
stream,
&ControlResponse::Enrolled(ClientEnrollmentResponse {
protocol_version: ProtocolVersion::V1,
credential,
}),
)
}
fn serve_registration(
&self,
stream: &mut rustls::StreamOwned<ServerConnection, std::os::unix::net::UnixStream>,
certificates: &[Vec<u8>],
) -> Result<(), ControlError> {
let [leaf] = certificates else {
return Err(ControlError::InvalidCertificateProfile);
};
let credential = self.registry.credential_for_leaf(leaf)?;
let request: ControlRequest = read_frame(stream, MAX_CONTROL_BYTES)?;
let ControlRequest::Register(request) = request else {
return Err(ControlError::InvalidRequest);
};
let capability = issue_session_capability(
request.session_binding.clone(),
self.anchor.coordinator_protocol_credential.credential_id,
self.anchor.sandbox_protocol_credential.credential_id,
&self.anchor.coordinator_protocol_key,
)?;
let registered = self.registry.register(request, &credential)?;
let result = (|| {
write_frame(
stream,
&ControlResponse::Registered(SessionRegistrationResponse {
protocol_version: ProtocolVersion::V1,
session_capability: capability,
}),
)?;
let mut extra = [0_u8; 1];
match stream.read(&mut extra) {
Ok(0) => Ok(()),
Ok(_) => Err(ControlError::InvalidRequest),
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(()),
Err(error) => Err(error.into()),
}
})();
let disconnect = self.registry.disconnect(registered.binding.session_id);
result.and(disconnect)
}
fn validate_publication(&self) -> Result<(), ControlError> {
self.paths.validate_root(self.uid)?;
let socket = std::fs::symlink_metadata(&self.paths.socket)?;
let identity = std::fs::symlink_metadata(&self.paths.identity)?;
if FileIdentity::from_metadata(&socket) != self.socket_identity
|| !socket.file_type().is_socket()
|| socket.uid() != self.uid
|| socket.mode() & 0o7777 != 0o600
|| FileIdentity::from_metadata(&identity) != self.identity_file_identity
|| !identity.file_type().is_file()
|| identity.uid() != self.uid
|| identity.mode() & 0o7777 != 0o600
|| usize::try_from(self.identity_file.metadata()?.len()).ok()
!= Some(self.publication_bytes.len())
{
return Err(ControlError::InvalidPublication(
"publication objects changed identity, ownership, or mode",
));
}
let mut exact_identity = self.identity_file.try_clone()?;
exact_identity.seek(SeekFrom::Start(0))?;
let mut bytes = Vec::new();
exact_identity.read_to_end(&mut bytes)?;
if bytes != self.publication_bytes {
return Err(ControlError::InvalidPublication(
"publication identity bytes changed",
));
}
self.registry.require_serviceable_generation()
}
/// Creates a TLS client configuration pinned to this publication identity.
///
/// # Errors
/// Refuses malformed CA or client certificate/key material.
pub fn client_config(
publication: &ProtocolPublicationIdentity,
client: Option<(&TlsCredential, PrivateKeyDer<'static>)>,
) -> Result<ClientConfig, ControlError> {
let mut roots = RootCertStore::empty();
roots.add(CertificateDer::from(
publication.tls_ca_certificate_der.clone(),
))?;
let builder = ClientConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_root_certificates(roots);
let mut config = if let Some((credential, private_key)) = client {
verify_tls_credential(
credential,
&publication.generation_identity,
TlsRole::ClientCli,
credential.subject_id(),
)?;
builder.with_client_auth_cert(
vec![CertificateDer::from(credential.certificate_der.clone())],
private_key,
)?
} else {
builder.with_no_client_auth()
};
config.alpn_protocols = vec![CONTROL_ALPN.to_vec()];
config.enable_early_data = false;
config.resumption = rustls::client::Resumption::disabled();
Ok(config)
}
/// Connects a rustls stream to the fixed control socket using an existing configuration.
///
/// # Errors
/// Returns path, connection, or TLS initialization failures.
pub fn connect(
socket_path: &Path,
config: ClientConfig,
publication: &ProtocolPublicationIdentity,
) -> Result<rustls::StreamOwned<ClientConnection, std::os::unix::net::UnixStream>, ControlError>
{
let socket = std::os::unix::net::UnixStream::connect(socket_path)?;
let server_name = ServerName::try_from(CONTROL_SERVER_NAME)
.map_err(|_| ControlError::InvalidCertificateProfile)?;
let mut connection = ClientConnection::new(Arc::new(config), server_name)?;
let mut socket = socket;
while connection.is_handshaking() {
connection.complete_io(&mut socket)?;
}
let [certificate] = connection.peer_certificates().unwrap_or_default() else {
return Err(ControlError::InvalidCertificateProfile);
};
if connection.protocol_version() != Some(rustls::ProtocolVersion::TLSv1_3)
|| connection.alpn_protocol() != Some(CONTROL_ALPN)
|| certificate_digest(certificate) != publication.coordinator_tls_certificate_sha256
{
return Err(ControlError::InvalidCertificateProfile);
}
Ok(rustls::StreamOwned::new(connection, socket))
}
/// Writes one length-delimited control record.
///
/// # Errors
/// Returns encoding or TLS I/O failures.
pub fn send<Request: serde::Serialize>(
stream: &mut impl Write,
request: &Request,
) -> Result<(), ControlError> {
write_frame(stream, request)
}
/// Reads one length-delimited control record.
///
/// # Errors
/// Returns framing, size, decoding, or TLS I/O failures.
pub fn receive<Response: serde::de::DeserializeOwned + serde::Serialize>(
stream: &mut impl Read,
) -> Result<Response, ControlError> {
read_frame(stream, MAX_CONTROL_BYTES)
}
}
impl Drop for PublishedControlFace {
fn drop(&mut self) {
remove_if_identity_matches(&self.paths.socket, self.socket_identity);
remove_if_identity_matches(&self.paths.identity, self.identity_file_identity);
}
}
fn remove_if_identity_matches(path: &Path, expected: FileIdentity) {
if std::fs::symlink_metadata(path)
.ok()
.is_some_and(|metadata| FileIdentity::from_metadata(&metadata) == expected)
{
let _ = std::fs::remove_file(path);
}
}
fn leaf_parameters(
common_name: &str,
usage: ExtendedKeyUsagePurpose,
now: OffsetDateTime,
) -> Result<CertificateParams, ControlError> {
let mut params = CertificateParams::new(vec![common_name.to_owned()])?;
params.distinguished_name = DistinguishedName::new();
params
.distinguished_name
.push(DnType::CommonName, common_name);
params.not_before = now - Duration::minutes(1);
params.not_after = now + Duration::hours(1);
params.is_ca = IsCa::NoCa;
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![usage];
Ok(params)
}
fn random_signing_key() -> Result<SigningKey, ControlError> {
let mut bytes = [0_u8; 32];
getrandom::fill(&mut bytes).map_err(|_| ControlError::InvalidCertificateProfile)?;
Ok(SigningKey::from_bytes(&bytes))
}
fn enrollment_binding(
generation_identity: &GenerationIdentity,
cli_id: ClientId,
) -> Result<String, ControlError> {
let encoded = encode(&(generation_identity, cli_id))?;
Ok(format!("lazaret-client-{}", hex(&sha256(&encoded))))
}
fn certificate_digest(certificate: impl AsRef<[u8]>) -> CertificateDigest {
CertificateDigest::new(sha256(certificate.as_ref()))
}
fn sha256(bytes: &[u8]) -> [u8; 32] {
Sha256::digest(bytes).into()
}
fn short_identity(bytes: &[u8; 32]) -> String {
hex(&bytes[..8])
}
fn hex(bytes: &[u8]) -> String {
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use fmt::Write as _;
let _ = write!(encoded, "{byte:02x}");
}
encoded
}
fn require_peer_uid(actual: u32, expected: u32) -> Result<(), ControlError> {
if actual == expected {
Ok(())
} else {
Err(ControlError::UnauthorizedPeer)
}
}
fn is_live_session(phase: SessionPhase) -> bool {
matches!(
phase,
SessionPhase::Registering
| SessionPhase::Attached
| SessionPhase::Prompting
| SessionPhase::HostExecuting
)
}
fn read_frame<Record: serde::de::DeserializeOwned + serde::Serialize>(
reader: &mut impl Read,
maximum: usize,
) -> Result<Record, ControlError> {
let mut length = [0_u8; 4];
reader.read_exact(&mut length)?;
let length = u32::from_be_bytes(length) as usize;
if length > maximum {
return Err(ControlError::OversizeRequest { length, maximum });
}
let mut bytes = vec![0_u8; length];
reader.read_exact(&mut bytes)?;
decode(&bytes).map_err(Into::into)
}
fn write_frame<Record: serde::Serialize>(
writer: &mut impl Write,
record: &Record,
) -> Result<(), ControlError> {
let bytes = encode(record)?;
let length = u32::try_from(bytes.len())
.map_err(|_| ControlError::OversizeRequest {
length: bytes.len(),
maximum: u32::MAX as usize,
})?
.to_be_bytes();
writer.write_all(&length)?;
writer.write_all(&bytes)?;
writer.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::thread;
use lazaret_core::identity::{
ConfigurationFingerprint, CoordinatorId, DomainId, GenerationId, SandboxId,
};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName};
use super::*;
fn generation() -> GenerationIdentity {
GenerationIdentity {
agent_profile: "trusted".into(),
domain_id: DomainId::new([1; 32]),
generation_id: GenerationId::new(7),
configuration_fingerprint: ConfigurationFingerprint::new([2; 32]),
coordinator_id: CoordinatorId::new([3; 32]),
sandbox_id: SandboxId::new([4; 32]),
}
}
fn client_config_with_certificate(
anchor: &TrustAnchor,
certificate: CertificateDer<'static>,
private_key: PrivatePkcs8KeyDer<'static>,
) -> ClientConfig {
let mut roots = RootCertStore::empty();
roots.add(anchor.tls_ca_der.clone()).unwrap();
let mut config = ClientConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_root_certificates(roots)
.with_client_auth_cert(vec![certificate], PrivateKeyDer::Pkcs8(private_key))
.unwrap();
config.alpn_protocols = vec![CONTROL_ALPN.to_vec()];
config.resumption = rustls::client::Resumption::disabled();
config
}
fn handshake_pair(
server_config: ServerConfig,
client_config: ClientConfig,
) -> (Result<(), rustls::Error>, Result<(), rustls::Error>) {
let (mut server_socket, mut client_socket) =
std::os::unix::net::UnixStream::pair().unwrap();
let server = thread::spawn(move || {
let mut connection = ServerConnection::new(Arc::new(server_config)).unwrap();
while connection.is_handshaking() {
connection
.complete_io(&mut server_socket)
.map_err(|error| rustls::Error::General(error.to_string()))?;
}
Ok(())
});
let mut connection = ClientConnection::new(
Arc::new(client_config),
ServerName::try_from(CONTROL_SERVER_NAME).unwrap(),
)
.unwrap();
let client = (|| {
while connection.is_handshaking() {
connection
.complete_io(&mut client_socket)
.map_err(|error| rustls::Error::General(error.to_string()))?;
}
Ok(())
})();
(server.join().unwrap(), client)
}
#[test]
fn exact_peer_uid_is_required() {
assert!(require_peer_uid(1_000, 1_000).is_ok());
assert!(matches!(
require_peer_uid(1_001, 1_000),
Err(ControlError::UnauthorizedPeer)
));
}
#[test]
fn rustls_refuses_client_leaf_with_server_role() {
let anchor = TrustAnchor::create(generation()).unwrap();
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
let certificate = leaf_parameters(
"wrong-client-role.invalid",
ExtendedKeyUsagePurpose::ServerAuth,
OffsetDateTime::now_utc(),
)
.unwrap()
.signed_by(&key, &anchor.tls_ca)
.unwrap();
let client_config = client_config_with_certificate(
&anchor,
certificate.der().clone(),
PrivatePkcs8KeyDer::from(key.serialize_der()),
);
let (server, client) = handshake_pair(anchor.server_config().unwrap(), client_config);
assert!(server.is_err() || client.is_err());
}
#[test]
fn rustls_refuses_server_leaf_with_client_role() {
let anchor = TrustAnchor::create(generation()).unwrap();
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
let certificate = leaf_parameters(
CONTROL_SERVER_NAME,
ExtendedKeyUsagePurpose::ClientAuth,
OffsetDateTime::now_utc(),
)
.unwrap()
.signed_by(&key, &anchor.tls_ca)
.unwrap();
let server_config =
ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_no_client_auth()
.with_single_cert(
vec![certificate.der().clone()],
PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der())),
)
.unwrap();
let client_config =
PublishedControlFace::client_config(&anchor.publication_identity(), None).unwrap();
let (server, client) = handshake_pair(server_config, client_config);
assert!(server.is_err() || client.is_err());
}
}