Complete authenticated interactive prompt routing

This commit is contained in:
ms2k 2026-07-17 07:20:21 +09:00
commit 46b52b4b43

View file

@ -4,18 +4,22 @@ use std::io;
use std::num::NonZeroU16;
use std::sync::{Arc, Mutex};
use ed25519_dalek::SigningKey;
use lazaret_core::identity::{
BootId, BundleDigest, ClientId, ConfigurationFingerprint, HostPath, HostPidLocator,
NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, ProcessStartTimeTicks,
SandboxId, SessionAnchorId,
RequestId, SandboxId, SessionAnchorId,
};
use lazaret_core::protocol::{VerifiedSession, encode_signed, issue_session_capability};
use lazaret_core::records::{
Domain, DurableDecision, Generation, Participant, SandboxOwnership, Session, TerminalInput,
TerminalSignal,
DeveloperDecisionKind, Domain, DurableDecision, Generation, GenerationIdentity, MailboxRequest,
Participant, SandboxOwnership, Session, SessionBinding, TerminalInput, TerminalSignal,
};
use lazaret_linux::filesystem::try_exclusive_flock;
use tempfile::TempDir;
use crate::control::TrustAnchor;
use super::*;
fn bytes(value: u8) -> [u8; 32] {
@ -533,6 +537,158 @@ fn engine_for(fixture: &Fixture) -> (Arc<MockFactory>, SessionTerminalEngine) {
engine_for_store(store)
}
#[test]
#[allow(clippy::too_many_lines)]
fn production_prompt_adapter_routes_exact_authenticated_denial() {
let generation = GenerationIdentity {
agent_profile: "trusted".into(),
domain_id: DomainId::new(bytes(1)),
generation_id: GenerationId::new(7),
configuration_fingerprint: ConfigurationFingerprint::new(bytes(2)),
coordinator_id: CoordinatorId::new(bytes(3)),
sandbox_id: SandboxId::new(bytes(5)),
};
let mut trust_anchor = TrustAnchor::create(generation.clone()).unwrap();
let publication = trust_anchor.publication_identity();
let coordinator_key = trust_anchor.coordinator_protocol_key();
let credentials = trust_anchor.take_sandbox_credential_bundle().unwrap();
let sandbox_key = SigningKey::from_bytes(
credentials
.sandbox_private_key
.as_slice()
.try_into()
.unwrap(),
);
let anchor_key = SigningKey::from_bytes(&bytes(61));
let binding = SessionBinding {
generation_identity: generation,
cli_id: ClientId::new(bytes(62)),
session_id: SessionId::new(bytes(63)),
session_anchor_public_key: anchor_key.verifying_key().to_bytes().to_vec(),
};
let capability = issue_session_capability(
binding.clone(),
credentials.coordinator_protocol_credential().credential_id,
credentials.sandbox_protocol_credential().credential_id,
&coordinator_key,
)
.unwrap();
let authority = VerifiedSession::new(
publication.trust_manifest_sha256,
credentials.trust_manifest.tls_ca_sha256,
&credentials.trust_manifest,
binding.clone(),
credentials.coordinator_protocol_credential(),
credentials.sandbox_protocol_credential(),
capability.clone(),
)
.unwrap();
let fixture = Fixture::new(&[]);
let session_anchor_id =
SessionAnchorId::new(Sha256::digest(&binding.session_anchor_public_key).into());
let mut session = Session {
generation_id: GenerationId::new(7),
session_id: binding.session_id,
cli_id: binding.cli_id,
session_anchor_id,
session_phase: SessionPhase::Registering,
};
fixture
.store
.apply(&[DomainStoreMutation::PutSession(session.clone())])
.unwrap();
session.session_phase = SessionPhase::Attached;
fixture
.store
.apply(&[DomainStoreMutation::PutSession(session)])
.unwrap();
let prompt_transport = Arc::new(SessionPromptTransport::new(Arc::clone(&fixture.store)));
let factory = Arc::new(MockFactory::default());
let mut engine = SessionTerminalEngine::new(
Arc::clone(&fixture.store),
factory,
prompt_transport.clone(),
);
engine.acquire(&acquisition(binding.session_id)).unwrap();
let request = MailboxRequest::new(
binding.clone(),
RequestId::new(bytes(64)),
capability,
credentials.sandbox_protocol_credential().credential_id,
vec![b"/usr/bin/tool".to_vec()],
"update host metadata".into(),
)
.unwrap();
let signed_request = encode_signed(&request, &sandbox_key).unwrap();
let expected_prompt = prompt_transport
.queue_signed_request(authority, &signed_request)
.unwrap();
let engine = Arc::new(Mutex::new(engine));
let mut control = RunscInteractiveControlSession {
engine: Arc::clone(&engine),
prompt_transport: prompt_transport.clone(),
generation_id: GenerationId::new(7),
session_id: binding.session_id,
};
let batch = control
.exchange(InteractiveSessionEvent::Poll {
session_id: binding.session_id,
})
.unwrap();
assert_eq!(
batch.messages,
vec![InteractiveSessionMessage::Prompt(expected_prompt.clone())]
);
let signed_decision = encode_signed(
&DeveloperDecision {
protocol_version: ProtocolVersion::V1,
session_binding: binding.clone(),
request_id: expected_prompt.request_id,
request_sha256: expected_prompt.request_sha256,
decision: DeveloperDecisionKind::Deny,
approved_command_argv: Vec::new(),
},
&anchor_key,
)
.unwrap();
control
.exchange(InteractiveSessionEvent::PromptDecision {
session_id: binding.session_id,
signed_decision: signed_decision.clone(),
})
.unwrap();
let routed = prompt_transport
.take_decision(binding.session_id)
.unwrap()
.unwrap();
assert_eq!(routed.signed_decision, signed_decision);
assert_eq!(routed.decision.decision, DeveloperDecisionKind::Deny);
assert_eq!(
fixture
.store
.snapshot()
.unwrap()
.sessions
.into_iter()
.find(|session| session.session_id == binding.session_id)
.unwrap()
.session_phase,
SessionPhase::Attached
);
engine
.lock()
.unwrap()
.handle(
GenerationId::new(7),
SessionTerminalEvent::Input(TerminalInput {
session_id: binding.session_id,
bytes: b"sandbox-input".to_vec(),
}),
)
.unwrap();
}
fn engine_for_store(
store: Arc<dyn TerminalDomainStore>,
) -> (Arc<MockFactory>, SessionTerminalEngine) {