Skip to main content

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}