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