weida_core/identity.rs
1//! Peer identity: the fingerprint of a certificate's public key.
2//!
3//! weida names a peer by what it can prove it holds — a private key — rather
4//! than by what a certificate authority says about it. The fingerprint is the
5//! SHA-256 digest of the DER-encoded `SubjectPublicKeyInfo` of the leaf
6//! certificate, the same value `openssl x509 -pubkey | openssl pkey -pubin
7//! -outform der | sha256sum` prints and the one HPKP and `curl --pinnedpubkey`
8//! pin. Hashing the key rather than the certificate keeps the fingerprint
9//! stable across certificate renewals that reuse the key.
10//!
11//! This module holds only the value type and its text form. Computing a
12//! fingerprint from certificate bytes needs a parser and a hash, both of which
13//! live in the transport crate.
14
15use std::fmt;
16use std::str::FromStr;
17
18use crate::error::Error;
19
20/// Text prefix of the canonical form; names the digest so the form can evolve.
21const PREFIX: &str = "sha256:";
22
23/// SHA-256 digest of a peer's public key.
24///
25/// Canonical text form: `sha256:` followed by 64 lowercase hex digits. Parsing
26/// accepts either case; nothing else. Equality is byte equality.
27#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct Fingerprint([u8; 32]);
29
30impl Fingerprint {
31 /// Wraps a digest already computed.
32 pub const fn from_bytes(bytes: [u8; 32]) -> Fingerprint {
33 Fingerprint(bytes)
34 }
35
36 /// The raw digest.
37 pub const fn as_bytes(&self) -> &[u8; 32] {
38 &self.0
39 }
40}
41
42/// A principal the **kernel** proved, on a local transport.
43///
44/// Captured at connect time and fixed for the life of the connection, which
45/// is what `SO_PEERCRED` and `LOCAL_PEERCRED` give and all they give
46/// (`docs/research/ipc.md` §1.5, §2.2). Two rules come with it
47/// ([decisions/0010](../../../docs/decisions/0010-local-transport.md) §4.4):
48/// the credential is the one taken at connect and never at send time, and a
49/// **PID is an observation** — it MUST NOT be the thing an application
50/// authorizes on, because it is reusable and racy where it exists at all.
51/// macOS reports no PID, so `pid` is `None` there.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub struct LocalPrincipal {
54 /// Effective user id of the peer process at connect time.
55 pub uid: u32,
56 /// Primary group id, where the platform reports one.
57 pub gid: u32,
58 /// Process id, where the platform reports one. An observation only.
59 pub pid: Option<u32>,
60}
61
62/// A principal the **kernel** proved, on a Windows named pipe.
63///
64/// The client's token SID, read through `ImpersonateNamedPipeClient` on the
65/// serving side and captured once at connect time
66/// (`docs/research/ipc.md` §3.3, [0010 §4.4]). The same two rules as
67/// [`LocalPrincipal`]: the credential is the one taken at connect, and the
68/// PID is an observation that MUST NOT be authorized on — `GetNamedPipe
69/// ClientProcessId` reports it and nothing signs it.
70///
71/// A separate type rather than a third field set on [`LocalPrincipal`],
72/// because a SID and a uid are not comparable values and a caller that
73/// authorizes on one must be made to say which. The SID is an `Arc<str>`
74/// rather than a `String` so that a [`PeerIdentity`] stays a refcount bump
75/// to clone: it is copied into every incoming transfer's metadata, and a
76/// string allocation there would be one per message.
77#[derive(Clone, Debug, PartialEq, Eq, Hash)]
78pub struct WindowsPrincipal {
79 /// The account SID in its string form (`S-1-5-21-...`).
80 pub sid: std::sync::Arc<str>,
81 /// The client process id, where the pipe reports one. An observation
82 /// only.
83 pub pid: Option<u32>,
84}
85
86/// Who the peer is, once it has been **proved**.
87///
88/// Two kinds of proof, never a claim
89/// ([decisions/0008](../../../docs/decisions/0008-session-identity.md) §4.1 as
90/// amended by [0010](../../../docs/decisions/0010-local-transport.md) §4.4): a
91/// key the peer demonstrated it holds in the TLS handshake, or a principal the
92/// kernel attributed to the process on the other end of a local connection.
93/// An anonymous TLS client and an in-process peer have neither, and are
94/// reported as `None` rather than as an empty identity.
95#[derive(Clone, Debug, PartialEq, Eq, Hash)]
96pub enum PeerIdentity {
97 /// The peer's public-key fingerprint, proved by the TLS handshake.
98 Key(Fingerprint),
99 /// The peer's local principal, proved by the kernel.
100 Local(LocalPrincipal),
101 /// The peer's Windows account, proved by the kernel through the pipe's
102 /// client token.
103 Windows(WindowsPrincipal),
104}
105
106impl PeerIdentity {
107 /// The proved key, if this identity is one.
108 pub fn key(&self) -> Option<Fingerprint> {
109 match self {
110 PeerIdentity::Key(fp) => Some(*fp),
111 PeerIdentity::Local(_) | PeerIdentity::Windows(_) => None,
112 }
113 }
114
115 /// The proved local principal, if this identity is one.
116 pub fn local(&self) -> Option<LocalPrincipal> {
117 match self {
118 PeerIdentity::Local(principal) => Some(*principal),
119 PeerIdentity::Key(_) | PeerIdentity::Windows(_) => None,
120 }
121 }
122
123 /// The proved Windows account, if this identity is one.
124 pub fn windows(&self) -> Option<&WindowsPrincipal> {
125 match self {
126 PeerIdentity::Windows(principal) => Some(principal),
127 PeerIdentity::Key(_) | PeerIdentity::Local(_) => None,
128 }
129 }
130}
131
132impl fmt::Display for PeerIdentity {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 PeerIdentity::Key(fp) => write!(f, "{fp}"),
136 PeerIdentity::Local(p) => match p.pid {
137 Some(pid) => write!(f, "uid:{} gid:{} pid:{pid}", p.uid, p.gid),
138 None => write!(f, "uid:{} gid:{}", p.uid, p.gid),
139 },
140 PeerIdentity::Windows(p) => match p.pid {
141 Some(pid) => write!(f, "sid:{} pid:{pid}", p.sid),
142 None => write!(f, "sid:{}", p.sid),
143 },
144 }
145 }
146}
147
148impl From<Fingerprint> for PeerIdentity {
149 fn from(fp: Fingerprint) -> PeerIdentity {
150 PeerIdentity::Key(fp)
151 }
152}
153
154impl fmt::Display for Fingerprint {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.write_str(PREFIX)?;
157 for b in self.0 {
158 write!(f, "{b:02x}")?;
159 }
160 Ok(())
161 }
162}
163
164impl fmt::Debug for Fingerprint {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 write!(f, "Fingerprint({self})")
167 }
168}
169
170impl FromStr for Fingerprint {
171 type Err = Error;
172
173 fn from_str(s: &str) -> Result<Fingerprint, Error> {
174 let invalid = || Error::InvalidFingerprint(s.to_owned());
175 let hex = s.strip_prefix(PREFIX).ok_or_else(invalid)?;
176 if hex.len() != 64 {
177 return Err(invalid());
178 }
179 let mut out = [0u8; 32];
180 for (i, pair) in hex.as_bytes().chunks(2).enumerate() {
181 let hi = hex_nibble(pair[0]).ok_or_else(invalid)?;
182 let lo = hex_nibble(pair[1]).ok_or_else(invalid)?;
183 out[i] = (hi << 4) | lo;
184 }
185 Ok(Fingerprint(out))
186 }
187}
188
189fn hex_nibble(c: u8) -> Option<u8> {
190 match c {
191 b'0'..=b'9' => Some(c - b'0'),
192 b'a'..=b'f' => Some(c - b'a' + 10),
193 b'A'..=b'F' => Some(c - b'A' + 10),
194 _ => None,
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 const DIGEST: [u8; 32] = [
203 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
204 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
205 0x1e, 0xff,
206 ];
207 const TEXT: &str = "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1eff";
208
209 #[test]
210 fn display_is_prefixed_lowercase_hex() {
211 assert_eq!(Fingerprint::from_bytes(DIGEST).to_string(), TEXT);
212 }
213
214 #[test]
215 fn parse_accepts_either_case_and_roundtrips() {
216 let fp: Fingerprint = TEXT.parse().unwrap();
217 assert_eq!(fp.as_bytes(), &DIGEST);
218 let upper = TEXT.to_uppercase().replace("SHA256", "sha256");
219 assert_eq!(upper.parse::<Fingerprint>().unwrap(), fp);
220 assert_eq!(fp.to_string().parse::<Fingerprint>().unwrap(), fp);
221 }
222
223 #[test]
224 fn parse_rejects_anything_else() {
225 let cases = [
226 "",
227 "sha256:",
228 "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1eff",
229 "sha1:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1eff",
230 "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1ef",
231 "sha256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1efff",
232 "sha256:0g0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1eff",
233 "SHA256:000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1eff",
234 ];
235 for c in cases {
236 assert!(
237 matches!(c.parse::<Fingerprint>(), Err(Error::InvalidFingerprint(_))),
238 "expected rejection of {c:?}"
239 );
240 }
241 }
242}