rightkit_daemon/contract/handler.rs
1use super::caller::Caller;
2use crate::error::ServiceError;
3use crate::host_identity::{
4 verify_peer_process, HostExpectation, HostIdentityError, HostIdentityReason, HostProcess,
5};
6use crate::peer::PeerIdentity;
7use serde_json::Value;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10
11/// Sends an `event` frame for the request being handled.
12pub type Emit = Arc<dyn Fn(Value) + Send + Sync>;
13
14/// What a handler knows about the request it is serving.
15#[derive(Clone)]
16pub struct CallContext {
17 conn: u64,
18 caller: Caller,
19 pub(crate) closed: Arc<AtomicBool>,
20 handoff: Arc<AtomicBool>,
21}
22
23pub(crate) fn new_context(
24 conn: u64,
25 caller: Caller,
26 closed: Arc<AtomicBool>,
27 handoff: Arc<AtomicBool>,
28) -> CallContext {
29 CallContext {
30 conn,
31 caller,
32 closed,
33 handoff,
34 }
35}
36
37impl CallContext {
38 pub fn conn_id(&self) -> u64 {
39 self.conn
40 }
41 /// Who is calling: a local app or a device.
42 pub fn origin(&self) -> &Caller {
43 &self.caller
44 }
45 /// The role of a device caller, from the scopes its enrolment granted (see
46 /// [`ROLE_SCOPE_PREFIX`](crate::contract::ROLE_SCOPE_PREFIX)). `None` for a local app.
47 pub fn role(&self) -> Option<&str> {
48 self.caller.role()
49 }
50 /// The app label the client declared in `hello` (already allowlisted); for a device caller,
51 /// the device id.
52 ///
53 /// The label is a claim: any process of the current user can declare any allowlisted
54 /// label. Use it to choose what a caller may ask for, never as proof of who is asking.
55 /// [`peer`](Self::peer) is what the OS verified.
56 pub fn app(&self) -> &str {
57 self.caller.label()
58 }
59 /// Who the OS says is on the other end of this connection, from the peer check that ran
60 /// before any byte was read: the uid on Unix, the user SID on Windows, and the process id
61 /// where the platform reports it (Linux, macOS and Windows). `None` for a caller that is not
62 /// a local app (a device has no OS peer).
63 ///
64 /// This is the fact, as opposed to the [`app`](Self::app) label. The user is always the
65 /// current user, since any other peer was refused. A pid identifies the process only while
66 /// it runs: record it for an audit trail, do not authorise by it later.
67 pub fn peer(&self) -> Option<&PeerIdentity> {
68 self.caller.peer()
69 }
70 /// Does the process on the other end of this connection run the program `expect` names?
71 /// The server-side counterpart of [`Client::connect_expecting`](crate::Client::connect_expecting):
72 /// the same [`HostExpectation`] (executable paths compared after canonicalisation, same file
73 /// by identity, then the optional [`HostExpectation::with_verifier`] check), applied to
74 /// [`peer`](Self::peer), so an app gates an owner-only method with RightKit code instead of
75 /// its own process inspection:
76 ///
77 /// ```text
78 /// let owner = HostExpectation::executable(app_exe);
79 /// if cx.verify_peer_executable(&owner).is_err() {
80 /// return Err(ServiceError::new(ERROR_FORBIDDEN, "owner only"));
81 /// }
82 /// ```
83 ///
84 /// Fails closed: a device caller (no OS peer), a peer whose pid, uid or executable cannot be
85 /// established, a process that exited or changed while it was identified, an expectation
86 /// naming no executable, a platform that cannot read a process's executable, a connection
87 /// that has closed, and a refusing verifier are all `Err`. Never `Ok` on a guess.
88 ///
89 /// Code signing: this crate links no code-signing API. On macOS the signing requirement
90 /// (`SecRequirementCreateWithString` against the process) and on Windows the Authenticode
91 /// subject (`WinVerifyTrust` on [`HostProcess::executable`]) go in
92 /// [`HostExpectation::with_verifier`], which runs after the executable matched. On macOS
93 /// [`HostProcess::audit_token`] is the token the kernel captured when the client connected,
94 /// the race-free handle for `SecCodeCopyGuestWithAttributes`.
95 ///
96 /// The process is pinned by pid plus its start time (Windows: an open process handle), and
97 /// the connection must still be open when the check ends, so a peer that exited and whose
98 /// pid was reused is refused. The window is the peer-gone watcher's lag, about 40 ms.
99 /// Run it per call; do not cache the answer past the connection.
100 pub fn verify_peer_executable(
101 &self,
102 expect: &HostExpectation,
103 ) -> Result<HostProcess, HostIdentityError> {
104 let host = verify_peer_process(self.peer(), expect)?;
105 if self.peer_closed() {
106 return Err(HostIdentityError {
107 expected: expect.executables().to_vec(),
108 found_pid: Some(host.pid),
109 found_executable: Some(host.executable),
110 reason: HostIdentityReason::HostChanged(
111 "the connection closed while the peer was identified".to_string(),
112 ),
113 });
114 }
115 Ok(host)
116 }
117 /// Is the process on the other end of this connection code-signed by an identity `policy`
118 /// names (macOS team identifier, Windows Authenticode signer)? The signature counterpart of
119 /// [`verify_peer_executable`](Self::verify_peer_executable), with the same pinning (pid plus
120 /// start time, macOS audit token) and the same fail-closed rules; see
121 /// [`verify_peer`](crate::verify_peer) for what each platform checks. A route that must come
122 /// from the signed owner (the licence snapshot delivery, rightkit#1501) calls it per request:
123 ///
124 /// ```text
125 /// let owner = PeerPolicy::signed_by(SigningIdentity::MacTeamId("ABCDE12345".into()));
126 /// if cx.verify_peer_signed(&owner).is_err() {
127 /// return Err(ServiceError::new(ERROR_FORBIDDEN, "owner only"));
128 /// }
129 /// ```
130 ///
131 /// Run it per call; do not cache the answer past the connection.
132 pub fn verify_peer_signed(
133 &self,
134 policy: &crate::PeerPolicy,
135 ) -> Result<HostProcess, HostIdentityError> {
136 let expect = crate::peer_signature::expectation(policy);
137 let host = verify_peer_process(self.peer(), &expect)?;
138 if self.peer_closed() {
139 return Err(HostIdentityError {
140 expected: expect.executables().to_vec(),
141 found_pid: Some(host.pid),
142 found_executable: Some(host.executable),
143 reason: HostIdentityReason::HostChanged(
144 "the connection closed while the peer was identified".to_string(),
145 ),
146 });
147 }
148 Ok(host)
149 }
150 /// The client went away while this request was running.
151 pub fn peer_closed(&self) -> bool {
152 self.closed.load(Ordering::SeqCst)
153 }
154 /// A higher-ranked host asked for the service: return
155 /// [`ServiceError::handoff`] at the next safe boundary.
156 pub fn handoff_requested(&self) -> bool {
157 self.handoff.load(Ordering::SeqCst)
158 }
159}
160
161pub trait Handler: Send + Sync + 'static {
162 /// Serve one allowlisted request. Events go to `emit`; a returned
163 /// retryable error is sent to the client as-is.
164 fn call(
165 &self,
166 cx: &CallContext,
167 method: &str,
168 params: &Value,
169 emit: &Emit,
170 ) -> Result<Value, ServiceError>;
171 /// A takeover was accepted: stop starting new steps, finish at a boundary.
172 fn on_handoff(&self) {}
173 /// A connection closed (also fired mid-request when the peer drops).
174 fn connection_closed(&self, _conn: u64) {}
175}