Skip to main content

cross_krb5/
lib.rs

1//! # Cross Platform Kerberos 5 Interface
2//!
3//! cross-krb5 is a safe interface for Kerberos 5 services on Windows
4//! and Unix. It provides most of the flexibility of using gssapi and
5//! sspi directly with a unified cross platform api.
6//!
7//! As well as providing a uniform API, services using cross-krb5
8//! should interoperate across all the supported OSes transparently,
9//! and should interoperate with other services assuming they are not
10//! platform specific.
11//!
12//! # Example
13//! ```no_run
14//! use bytes::Bytes;
15//! use cross_krb5::{AcceptFlags, ClientCtx, InitiateFlags, K5Ctx, Step, ServerCtx};
16//! use std::{env::args, process::exit, sync::mpsc, thread};
17//!
18//! enum Msg {
19//!     Token(Bytes),
20//!     Msg(Bytes),
21//! }
22//!
23//! fn server(spn: String, input: mpsc::Receiver<Msg>, output: mpsc::Sender<Msg>) {
24//!     let mut server = ServerCtx::new(AcceptFlags::empty(), Some(&spn), None).expect("new");
25//!     let mut server = loop {
26//!         let token = match input.recv().expect("expected data") {
27//!             Msg::Msg(_) => panic!("server not finished initializing"),
28//!             Msg::Token(t) => t,
29//!         };
30//!         match server.step(&*token).expect("step") {
31//!             Step::Finished((ctx, token)) => {
32//!                 if let Some(token) = token {
33//!                     output.send(Msg::Token(Bytes::copy_from_slice(&*token))).expect("send");
34//!                 }
35//!                 break ctx
36//!             },
37//!             Step::Continue((ctx, token)) => {
38//!                 output.send(Msg::Token(Bytes::copy_from_slice(&*token))).expect("send");
39//!                 server = ctx;
40//!             }
41//!         }
42//!     };
43//!     match input.recv().expect("expected data msg") {
44//!         Msg::Token(_) => panic!("unexpected extra token"),
45//!         Msg::Msg(secret_msg) => println!(
46//!             "{}",
47//!             String::from_utf8_lossy(&server.unwrap(&*secret_msg).expect("unwrap"))
48//!         ),
49//!     }
50//! }
51//!
52//! fn client(spn: &str, input: mpsc::Receiver<Msg>, output: mpsc::Sender<Msg>) {
53//!     let (mut client, token) =
54//!         ClientCtx::new(InitiateFlags::empty(), None, spn, None).expect("new");
55//!     output.send(Msg::Token(Bytes::copy_from_slice(&*token))).expect("send");
56//!     let mut client = loop {
57//!         let token = match input.recv().expect("expected data") {
58//!             Msg::Msg(_) => panic!("client not finished initializing"),
59//!             Msg::Token(t) => t,
60//!         };
61//!         match client.step(&*token).expect("step") {
62//!             Step::Finished((ctx, token)) => {
63//!                 if let Some(token) = token {
64//!                     output.send(Msg::Token(Bytes::copy_from_slice(&*token))).expect("send");
65//!                 }
66//!                 break ctx
67//!             },
68//!             Step::Continue((ctx, token)) => {
69//!                 output.send(Msg::Token(Bytes::copy_from_slice(&*token))).expect("send");
70//!                 client = ctx;
71//!             }
72//!         }
73//!     };
74//!     let msg = client.wrap(true, b"super secret message").expect("wrap");
75//!     output.send(Msg::Msg(Bytes::copy_from_slice(&*msg))).expect("send");
76//! }
77//!
78//! fn main() {
79//!     let args = args().collect::<Vec<_>>();
80//!     if args.len() != 2 {
81//!         println!("usage: {}: <service/host@REALM>", args[0]);
82//!         exit(1);
83//!     }
84//!     let spn = String::from(&args[1]);
85//!     let (server_snd, server_recv) = mpsc::channel();
86//!     let (client_snd, client_recv) = mpsc::channel();
87//!     thread::spawn(move || server(spn, server_recv, client_snd));
88//!     client(&args[1], client_recv, server_snd);
89//! }
90//! ```
91
92#[macro_use]
93extern crate bitflags;
94use anyhow::Result;
95use bytes::{Buf, BytesMut};
96use std::{ops::Deref, time::Duration};
97#[cfg(windows)]
98use ::windows::Win32::Security::Credentials::SecHandle;
99
100pub trait K5Cred: Sized {
101    fn server_acquire(
102        _flags: AcceptFlags,
103        principal: Option<&str>,
104    ) -> Result<Self>;
105
106    fn client_acquire(
107        _flags: InitiateFlags,
108        principal: Option<&str>,
109    ) -> Result<Self>;
110}
111
112pub trait K5Ctx {
113    type Buffer: Deref<Target = [u8]> + Send + Sync;
114    type IOVBuffer: Buf + Send + Sync;
115
116    /// Wrap the specified message for sending to the other side. If
117    /// `encrypt` is true then the contents will be encrypted. Even if
118    /// `encrypt` is false the integrity of the contents are
119    /// protected, if the message is altered in transit the other side
120    /// will know.
121    fn wrap(&mut self, encrypt: bool, msg: &[u8]) -> Result<Self::Buffer>;
122
123    /// Wrap data in place using the underlying wrap_iov facility. If
124    /// `encrypt` is true then the contents of `data` will be
125    /// encrypted in place. The returned buffer is NOT contiguous, and
126    /// as such you must use some kind of `writev` implementation to
127    /// properly send it. You can use tokio's `write_buf` directly, or
128    /// you can extract the iovecs for a direct call to `writev` using
129    /// `bytes::Buf::chunks_vectored`.
130    ///
131    /// If feature `iov` isn't enabled (it's in the default set)
132    /// then the underlying functionaly will be emulated, and there
133    /// will be no performance gain. `iov` is currently not
134    /// available on Mac OS, and compilation will fail if you try to
135    /// enable it. On OSes where it is supported using
136    /// wrap_iov/unwrap_iov is generally in the neighborhood of 2x to
137    /// 3x faster than wrap/unwrap.
138    fn wrap_iov(&mut self, encrypt: bool, msg: BytesMut) -> Result<Self::IOVBuffer>;
139
140    /// Unwrap the specified message returning it's decrypted and
141    /// verified contents
142    fn unwrap(&mut self, msg: &[u8]) -> Result<Self::Buffer>;
143
144    /// Unwrap in place the message at the beginning of the specified
145    /// `BytesMut` and then split it off and return it. This won't
146    /// copy or allocate, it just looks that way because the bytes
147    /// crate is awesome.
148    fn unwrap_iov(&mut self, len: usize, msg: &mut BytesMut) -> Result<BytesMut>;
149
150    /// Return the remaining time this session has to live
151    fn ttl(&mut self) -> Result<Duration>;
152}
153
154pub trait K5ServerCtx: K5Ctx {
155    /// Return the user principal name of the client context
156    /// associated with this server context.
157    ///
158    /// The name is the cryptographically authenticated client identity
159    /// (it can only be read once the context is established). The exact
160    /// string format is *not* guaranteed to be byte-identical across
161    /// platforms: on Unix it is the gssapi display name, while on Windows
162    /// it is the principal name (CNAME) from the Kerberos ticket as
163    /// formatted by SSPI. Both are typically `user@REALM`, but do not rely
164    /// on exact equality (e.g. realm casing) when comparing across OSes.
165    fn client(&mut self) -> Result<String>;
166}
167
168#[cfg(unix)]
169mod unix;
170
171#[cfg(unix)]
172use crate::unix::{
173    ClientCtx as ClientCtxImpl, Cred as CredImpl,
174    PendingClientCtx as PendingClientCtxImpl, PendingServerCtx as PendingServerCtxImpl,
175    ServerCtx as ServerCtxImpl
176};
177
178#[cfg(windows)]
179mod windows;
180
181#[cfg(windows)]
182use crate::windows::{
183    ClientCtx as ClientCtxImpl, PendingClientCtx as PendingClientCtxImpl,
184    PendingServerCtx as PendingServerCtxImpl, ServerCtx as ServerCtxImpl,
185    Cred as CredImpl
186};
187
188pub enum Step<C, T> {
189    Finished(C),
190    Continue(T),
191}
192
193#[derive(Debug)]
194pub struct Cred(CredImpl);
195
196impl K5Cred for Cred {
197    fn server_acquire(flags: AcceptFlags, principal: Option<&str>) -> Result<Cred> {
198        CredImpl::server_acquire(flags, principal).map(Cred)
199    }
200    fn client_acquire(flags: InitiateFlags, principal: Option<&str>) -> Result<Cred> {
201        CredImpl::client_acquire(flags, principal).map(Cred)
202    }
203}
204
205impl From<CredImpl> for Cred {
206    fn from(cred: CredImpl) -> Self {
207        Cred(cred)
208    }
209}
210
211#[cfg(unix)]
212impl From<libgssapi::credential::Cred> for Cred {
213    fn from(value: libgssapi::credential::Cred) -> Self {
214        Cred(CredImpl::from(value))
215    }
216}
217
218#[cfg(unix)]
219impl Into<libgssapi::credential::Cred> for Cred {
220    fn into(self) -> libgssapi::credential::Cred {
221        self.0.into()
222    }
223}
224
225#[cfg(windows)]
226impl Cred {
227    /// Wrap a raw SSPI credential handle, taking ownership of it.
228    ///
229    /// # Safety
230    ///
231    /// `handle` must be a live credential handle obtained from SSPI (e.g.
232    /// from `AcquireCredentialsHandle`) whose ownership is being transferred
233    /// to the returned `Cred`; dropping the `Cred` calls
234    /// `FreeCredentialsHandle` on it. The handle must not be used or freed
235    /// elsewhere. Passing a fabricated, borrowed, or already-freed handle is
236    /// undefined behavior. This mirrors libgssapi's `Cred::from_c`, which is
237    /// likewise unsafe for the same reason.
238    pub unsafe fn from_raw(handle: SecHandle) -> Cred {
239        Cred(CredImpl::from(handle))
240    }
241}
242
243#[cfg(windows)]
244impl Into<SecHandle> for Cred {
245    fn into(self) -> SecHandle {
246        self.0.into()
247    }
248}
249
250/// a partly initialized client context
251pub struct PendingClientCtx(PendingClientCtxImpl);
252
253impl PendingClientCtx {
254    /// Feed the server provided token to the client context,
255    /// performing one step of the initialization. If the
256    /// initialization is complete then return the established context
257    /// and optionally a final token that must be sent to the server,
258    /// otherwise return the pending context and another token to pass
259    /// to the server.
260    pub fn step(
261        self,
262        token: &[u8],
263    ) -> Result<
264        Step<
265            (ClientCtx, Option<impl Deref<Target = [u8]>>),
266            (PendingClientCtx, impl Deref<Target = [u8]>),
267        >,
268    > {
269        Ok(match self.0.step(token)? {
270            Step::Finished((ctx, tok)) => {
271                Step::Finished((ClientCtx(ctx), tok))
272            }
273            Step::Continue((ctx, tok)) => {
274                Step::Continue((PendingClientCtx(ctx), tok))
275            }
276        })
277    }
278}
279
280bitflags! {
281    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
282    pub struct InitiateFlags: u32 {
283        /// Windows only, use the sspi negotiate package instead of
284        /// the Kerberos package. Some Windows servers expect these
285        /// tokens instead of normal gssapi compatible tokens.
286        const NEGOTIATE_TOKEN = 0x1;
287        /// Opt out of mutual authentication. By default the client
288        /// requests mutual authentication and context establishment
289        /// fails unless the mechanism actually grants it; set this flag
290        /// to neither request nor require it.
291        const DISABLE_MUTUAL_AUTH = 0x2;
292        /// Opt out of confidentiality. By default the client requests
293        /// confidentiality, requires that it was granted, and permits
294        /// `wrap`/`wrap_iov` with `encrypt = true`. Set this flag to
295        /// neither request nor require it; `wrap`/`wrap_iov` with
296        /// `encrypt = true` then fail (the channel is integrity-only).
297        const DISABLE_CONFIDENTIALITY = 0x4;
298    }
299}
300
301impl Default for InitiateFlags {
302    /// The secure default: nothing is disabled, so both mutual
303    /// authentication and confidentiality are required.
304    fn default() -> Self {
305        InitiateFlags::empty()
306    }
307}
308
309/// A Kerberos client context
310#[derive(Debug)]
311pub struct ClientCtx(ClientCtxImpl);
312
313impl ClientCtx {
314    /// create a new client context for speaking to
315    /// `target_principal`. If `principal` is `None` then the
316    /// credentials of the user running current process will be
317    /// used. `target_principal` must be the service principal name of
318    /// the service you intend to communicate with. This should be an
319    /// spn as described by GSSAPI, e.g. `service/host@REALM`. If
320    /// present, `channel_bindings` is a service-specific channel
321    /// binding token which will be part of the initial communication
322    /// with the server.
323    ///
324    /// On success a `PendingClientCtx` and a token to be sent to the
325    /// server will be returned. The server and client may generate
326    /// multiple additional tokens, which must be passed to the their
327    /// respective `step` methods until the initialization is
328    /// complete.
329    pub fn new(
330        flags: InitiateFlags,
331        principal: Option<&str>,
332        target_principal: &str,
333        channel_bindings: Option<&[u8]>,
334    ) -> Result<(PendingClientCtx, impl Deref<Target = [u8]>)> {
335        let (pending, token) =
336            ClientCtxImpl::new(flags, principal, target_principal, channel_bindings)?;
337        Ok((PendingClientCtx(pending), token))
338    }
339
340    pub fn new_with_cred(
341        flags: InitiateFlags,
342        cred: Cred,
343        target_principal: &str,
344        channel_bindings: Option<&[u8]>
345    ) -> Result<(PendingClientCtx, impl Deref<Target=[u8]>)> {
346        let (pending, token) =
347            ClientCtxImpl::new_with_cred(flags, cred.0, target_principal, channel_bindings)?;
348        Ok((PendingClientCtx(pending), token))
349    }
350}
351
352impl K5Ctx for ClientCtx {
353    type Buffer = <ClientCtxImpl as K5Ctx>::Buffer;
354    type IOVBuffer = <ClientCtxImpl as K5Ctx>::IOVBuffer;
355
356    fn wrap(&mut self, encrypt: bool, msg: &[u8]) -> Result<Self::Buffer> {
357        K5Ctx::wrap(&mut self.0, encrypt, msg)
358    }
359
360    fn wrap_iov(&mut self, encrypt: bool, msg: BytesMut) -> Result<Self::IOVBuffer> {
361        K5Ctx::wrap_iov(&mut self.0, encrypt, msg)
362    }
363
364    fn unwrap(&mut self, msg: &[u8]) -> Result<Self::Buffer> {
365        K5Ctx::unwrap(&mut self.0, msg)
366    }
367
368    fn unwrap_iov(&mut self, len: usize, msg: &mut BytesMut) -> Result<BytesMut> {
369        K5Ctx::unwrap_iov(&mut self.0, len, msg)
370    }
371
372    fn ttl(&mut self) -> Result<Duration> {
373        K5Ctx::ttl(&mut self.0)
374    }
375}
376
377bitflags! {
378    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
379    pub struct AcceptFlags: u32 {
380        /// Windows only, use the sspi negotiate package instead of
381        /// the Kerberos package. Some Windows clients generate these
382        /// tokens instead of normal gssapi compatible tokens. This
383        /// likely won't be able to parse gssapi tokens, so only use
384        /// this if you know the client will be on windows sending
385        /// negotiate tokens.
386        const NEGOTIATE_TOKEN = 0x1;
387        /// Opt out of requiring mutual authentication. By default
388        /// accepting a context that did not establish mutual
389        /// authentication fails; set this flag to not require it.
390        const DISABLE_MUTUAL_AUTH = 0x2;
391        /// Opt out of confidentiality. By default the acceptor requires
392        /// that confidentiality was granted and permits `wrap`/`wrap_iov`
393        /// with `encrypt = true`. Set this flag to not require it;
394        /// `wrap`/`wrap_iov` with `encrypt = true` then fail.
395        const DISABLE_CONFIDENTIALITY = 0x4;
396    }
397}
398
399impl Default for AcceptFlags {
400    /// The secure default: nothing is disabled, so both mutual
401    /// authentication and confidentiality are required.
402    fn default() -> Self {
403        AcceptFlags::empty()
404    }
405}
406
407pub struct PendingServerCtx(PendingServerCtxImpl);
408
409impl PendingServerCtx {
410    pub fn step(
411        self,
412        token: &[u8],
413    ) -> Result<
414        Step<
415            (ServerCtx, Option<impl Deref<Target = [u8]>>),
416            (PendingServerCtx, impl Deref<Target = [u8]>),
417        >,
418    > {
419        Ok(match self.0.step(token)? {
420            Step::Finished((ctx, tok)) => {
421                Step::Finished((ServerCtx(ctx), tok))
422            }
423            Step::Continue((ctx, tok)) => {
424                Step::Continue((PendingServerCtx(ctx), tok))
425            }
426        })
427    }
428}
429
430/// A Kerberos server context
431#[derive(Debug)]
432pub struct ServerCtx(ServerCtxImpl);
433
434impl ServerCtx {
435    /// Create a new server context for `principal`, which should be
436    /// the service principal name assigned to the service the client
437    /// will be requesting. If it is left as `None` it will use the
438    /// user running the current process. The returned pending context
439    /// must be initiaized by exchanging one or more tokens with the
440    /// client before it can be used.
441    ///
442    /// If present, `channel_bindings` must match the bindings the client
443    /// supplied to `ClientCtx::new`; the mechanism rejects the context
444    /// otherwise. Note that with a `None` acceptor binding the mechanism
445    /// generally accepts whatever the client sent, so to actually enforce
446    /// channel binding the server must pass its own expected bindings here.
447    pub fn new(
448        flags: AcceptFlags,
449        principal: Option<&str>,
450        channel_bindings: Option<&[u8]>,
451    ) -> Result<PendingServerCtx> {
452        Ok(PendingServerCtx(ServerCtxImpl::new(flags, principal, channel_bindings)?))
453    }
454
455    pub fn new_with_cred(
456        flags: AcceptFlags,
457        cred: Cred,
458        channel_bindings: Option<&[u8]>,
459    ) -> Result<PendingServerCtx> {
460        Ok(PendingServerCtx(ServerCtxImpl::new_with_cred(
461            flags,
462            cred.0,
463            channel_bindings,
464        )?))
465    }
466}
467
468impl K5Ctx for ServerCtx {
469    type Buffer = <ServerCtxImpl as K5Ctx>::Buffer;
470    type IOVBuffer = <ServerCtxImpl as K5Ctx>::IOVBuffer;
471
472    fn wrap(&mut self, encrypt: bool, msg: &[u8]) -> Result<Self::Buffer> {
473        K5Ctx::wrap(&mut self.0, encrypt, msg)
474    }
475
476    fn wrap_iov(&mut self, encrypt: bool, msg: BytesMut) -> Result<Self::IOVBuffer> {
477        K5Ctx::wrap_iov(&mut self.0, encrypt, msg)
478    }
479
480    fn unwrap(&mut self, msg: &[u8]) -> Result<Self::Buffer> {
481        K5Ctx::unwrap(&mut self.0, msg)
482    }
483
484    fn unwrap_iov(&mut self, len: usize, msg: &mut BytesMut) -> Result<BytesMut> {
485        K5Ctx::unwrap_iov(&mut self.0, len, msg)
486    }
487
488    fn ttl(&mut self) -> Result<Duration> {
489        K5Ctx::ttl(&mut self.0)
490    }
491}
492
493impl K5ServerCtx for ServerCtx {
494    fn client(&mut self) -> Result<String> {
495        K5ServerCtx::client(&mut self.0)
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    // The flags are opt-out: the empty/default set must never disable a
504    // security property, so the easiest-to-write call is the secure one.
505    #[test]
506    fn secure_by_default() {
507        for f in [InitiateFlags::empty(), InitiateFlags::default()] {
508            assert!(!f.contains(InitiateFlags::DISABLE_MUTUAL_AUTH));
509            assert!(!f.contains(InitiateFlags::DISABLE_CONFIDENTIALITY));
510        }
511        for f in [AcceptFlags::empty(), AcceptFlags::default()] {
512            assert!(!f.contains(AcceptFlags::DISABLE_MUTUAL_AUTH));
513            assert!(!f.contains(AcceptFlags::DISABLE_CONFIDENTIALITY));
514        }
515    }
516}