Skip to main content

kipuka/auth/
gssapi.rs

1//! GSSAPI/Kerberos authentication for EST endpoints.
2//!
3//! Implements the `Authorization: Negotiate` (SPNEGO) authentication
4//! mechanism, following the same pattern as Akamu's GSSAPI support.
5//!
6//! When the `gssapi` feature is enabled, Kerberos tickets are
7//! cryptographically verified via the system's GSS-API library
8//! (`libgssapi` crate wrapping `libgssapi_krb5.so`).  Without the
9//! feature, only structural parsing of the SPNEGO token is available
10//! (extracting the service name from the cleartext ticket fields).
11//!
12//! Channel binding to the TLS session (tls-server-end-point, RFC 5929)
13//! is supported to prevent credential forwarding attacks.
14
15use std::sync::Arc;
16
17use axum::http::header::AUTHORIZATION;
18use axum::http::request::Parts;
19use axum::http::{HeaderValue, StatusCode};
20use axum::response::{IntoResponse, Response};
21use base64::Engine as _;
22use tracing::{debug, warn};
23
24use super::{AuthMethod, AuthResult};
25use crate::state::AppState;
26
27/// Request extension carrying the GSSAPI mutual-auth output token.
28///
29/// When `gss_accept_sec_context` produces an output token (i.e., the
30/// client requested mutual authentication), this extension is inserted
31/// into the request so handlers can include it in the response.
32///
33/// The inner [`HeaderValue`] is pre-formatted as `"Negotiate <base64>"`.
34#[derive(Clone)]
35pub struct NegotiateOutToken(pub HeaderValue);
36
37/// Response slot shared across extraction and outer HTTP response middleware.
38#[derive(Clone, Default)]
39pub(crate) struct NegotiateResponse(pub Arc<parking_lot::Mutex<Option<HeaderValue>>>);
40
41pub(crate) async fn response_token(
42    mut request: axum::extract::Request,
43    next: axum::middleware::Next,
44) -> Response {
45    let slot = NegotiateResponse::default();
46    request.extensions_mut().insert(slot.clone());
47    let mut response = next.run(request).await;
48    if let Some(token) = slot.0.lock().take() {
49        response
50            .headers_mut()
51            .insert(axum::http::header::WWW_AUTHENTICATE, token);
52    }
53    response
54}
55
56/// TLS channel binding data (tls-server-end-point, RFC 5929).
57///
58/// Injected into request extensions by the TLS accept loop.  Used to
59/// bind the GSSAPI context to the TLS session, preventing relay attacks.
60#[derive(Clone)]
61pub struct TlsChannelBinding(pub Vec<u8>);
62
63/// Attempt to extract and validate GSSAPI/SPNEGO credentials.
64///
65/// Returns:
66/// - `Some(Ok(AuthResult))` — GSSAPI authentication succeeded
67/// - `Some(Err(Response))` — Negotiate header present but invalid (401/403)
68/// - `None` — no Negotiate header present (try next auth method)
69pub async fn try_extract_gssapi(
70    parts: &mut Parts,
71    app: &Arc<AppState>,
72) -> Option<Result<AuthResult, Response>> {
73    let auth_header = parts.headers.get(AUTHORIZATION)?.to_str().ok()?;
74
75    // Only handle Negotiate tokens; Basic is handled by the OTP module.
76    let token_b64 = auth_header.strip_prefix("Negotiate ")?;
77
78    // Check that GSSAPI is configured.
79    let gss_cred = match app.gss_cred.as_ref() {
80        Some(cred) => Arc::clone(cred),
81        None => {
82            debug!("Negotiate header present but GSSAPI not configured");
83            return Some(Err((
84                StatusCode::UNAUTHORIZED,
85                "GSSAPI is not configured on this server",
86            )
87                .into_response()));
88        }
89    };
90
91    // Safety gate: reject structural-only parsing when crypto verification
92    // is required (the default) AND the `gssapi` feature is not compiled in.
93    // When the `gssapi` feature IS enabled, we use the real GSS-API library
94    // to verify tickets cryptographically, so no gate is needed.
95    #[cfg(not(feature = "gssapi"))]
96    if app.gssapi_require_crypto {
97        warn!(
98            "GSSAPI authentication rejected: `require_crypto_verification` is true (the default) \
99             but the `gssapi` feature is not compiled in.  Structural parsing of Kerberos tokens \
100             cannot verify client identity.  Either compile with `--features gssapi` or set \
101             `require_crypto_verification = false` in [admin.gssapi] for development/logging."
102        );
103        return Some(Err((
104            StatusCode::FORBIDDEN,
105            "GSSAPI authentication requires the `gssapi` feature; \
106             structural-only parsing is disabled by default",
107        )
108            .into_response()));
109    }
110
111    // Decode the base64 SPNEGO token.
112    let token_bytes = match base64::engine::general_purpose::STANDARD.decode(token_b64) {
113        Ok(t) => t,
114        Err(_) => {
115            return Some(Err((
116                StatusCode::BAD_REQUEST,
117                "malformed Negotiate token encoding",
118            )
119                .into_response()));
120        }
121    };
122
123    // Reject oversized tokens (128 KiB limit, matching Akamu).
124    const MAX_TOKEN_BYTES: usize = 128 * 1024;
125    if token_bytes.len() > MAX_TOKEN_BYTES {
126        return Some(Err((
127            StatusCode::BAD_REQUEST,
128            "Negotiate token exceeds size limit",
129        )
130            .into_response()));
131    }
132
133    // Extract TLS channel binding data for tls-server-end-point binding.
134    let channel_binding: Option<Vec<u8>> = parts
135        .extensions
136        .get::<TlsChannelBinding>()
137        .map(|b| b.0.clone());
138
139    if app.gssapi_require_crypto && channel_binding.is_none() {
140        return Some(Err((
141            StatusCode::FORBIDDEN,
142            "GSSAPI requires TLS channel binding",
143        )
144            .into_response()));
145    }
146
147    // Use spawn_blocking for the synchronous GSSAPI FFI call so we do not
148    // block a tokio worker thread.
149    let binding_owned = channel_binding;
150    let token_owned = token_bytes;
151    let require_crypto = app.gssapi_require_crypto;
152    let result = tokio::task::spawn_blocking(move || {
153        negotiate_accept(
154            gss_cred.as_ref(),
155            &token_owned,
156            binding_owned.as_deref(),
157            require_crypto,
158        )
159    })
160    .await;
161
162    let negotiate_result = match result {
163        Ok(r) => r,
164        Err(e) => {
165            tracing::error!(error = %e, "GSSAPI spawn_blocking panicked");
166            return Some(Err(StatusCode::INTERNAL_SERVER_ERROR.into_response()));
167        }
168    };
169
170    match negotiate_result {
171        Ok(NegotiateSuccess {
172            principal,
173            out_token,
174        }) => {
175            debug!(principal = %principal, "GSSAPI authentication succeeded");
176
177            // Store the output token (if any) for mutual authentication.
178            if !out_token.is_empty() {
179                let b64 = base64::engine::general_purpose::STANDARD.encode(&out_token);
180                if let Ok(hv) = HeaderValue::from_str(&format!("Negotiate {b64}")) {
181                    if let Some(slot) = parts.extensions.get::<NegotiateResponse>() {
182                        *slot.0.lock() = Some(hv.clone());
183                    }
184                    parts.extensions.insert(NegotiateOutToken(hv));
185                }
186            }
187
188            Some(Ok(AuthResult {
189                identity: principal,
190                method: AuthMethod::Gssapi,
191                client_cert_der: None,
192                subject_dn: None,
193                subject_alt_names: Vec::new(),
194                extended_key_usage: Vec::new(),
195            }))
196        }
197        Err(NegotiateError::Continue(out_token)) => {
198            // Multi-leg SPNEGO: return 401 with continuation token.
199            let b64 = base64::engine::general_purpose::STANDARD.encode(&out_token);
200            let mut resp = (StatusCode::UNAUTHORIZED, "").into_response();
201            if let Ok(hv) = HeaderValue::from_str(&format!("Negotiate {b64}")) {
202                resp.headers_mut().insert("WWW-Authenticate", hv);
203            }
204            Some(Err(resp))
205        }
206        Err(NegotiateError::Failed(msg)) => {
207            warn!(error = %msg, "GSSAPI authentication failed");
208            Some(Err(
209                (StatusCode::FORBIDDEN, "GSSAPI authentication failed").into_response()
210            ))
211        }
212    }
213}
214
215/// Build a 401 response with a `WWW-Authenticate: Negotiate` challenge.
216///
217/// Used when GSSAPI is configured but the client has not sent a Negotiate
218/// token.  Prompts the client to initiate a SPNEGO exchange.
219pub fn negotiate_challenge() -> Response {
220    let mut resp = (StatusCode::UNAUTHORIZED, "").into_response();
221    resp.headers_mut()
222        .insert("WWW-Authenticate", HeaderValue::from_static("Negotiate"));
223    resp
224}
225
226// ── Internal types ───────────────────────────────────────────────────────────
227
228struct NegotiateSuccess {
229    principal: String,
230    out_token: Vec<u8>,
231}
232
233#[allow(dead_code)]
234enum NegotiateError {
235    /// Multi-leg exchange: needs another round-trip with this output token.
236    Continue(Vec<u8>),
237    /// Authentication failed with the given reason.
238    Failed(String),
239}
240
241/// Synchronous SPNEGO token validation.
242///
243/// This function wraps the GSS-API context establishment and is designed
244/// to run inside `spawn_blocking`.
245///
246/// When the `gssapi` feature is enabled and `require_crypto` is `true`,
247/// the token is verified cryptographically via `gss_accept_sec_context`
248/// (libgssapi FFI), producing a verified client principal and an optional
249/// mutual-authentication output token.
250///
251/// When `require_crypto` is `false` (or the `gssapi` feature is not
252/// compiled in), the function falls back to structural parsing via
253/// `synta_krb5` — extracting the service name (sname) from the cleartext
254/// portion of the Kerberos ticket.  This cannot authenticate the client.
255///
256/// ## Token structure (RFC 4178 / RFC 4121)
257///
258/// The incoming token is an SPNEGO `NegotiationToken` (APPLICATION 0
259/// envelope):
260///
261/// ```text
262/// 60 <len> 06 09 <SPNEGO OID> <NegTokenInit DER>
263/// ```
264///
265/// The `NegTokenInit.mechToken` contains a Kerberos 5 GSS initial context
266/// token:
267///
268/// ```text
269/// 60 <len> 06 09 <KRB5 OID> 01 00 <AP-REQ DER>
270/// ```
271fn negotiate_accept(
272    cred: &dyn std::any::Any,
273    token: &[u8],
274    channel_binding: Option<&[u8]>,
275    require_crypto: bool,
276) -> Result<NegotiateSuccess, NegotiateError> {
277    // ── Cryptographic verification via libgssapi ─────────────────────────
278    #[cfg(feature = "gssapi")]
279    if require_crypto {
280        return verify_gssapi_token(cred, token, channel_binding);
281    }
282
283    // ── Structural-only fallback (development / logging mode) ────────────
284    let _ = require_crypto; // suppress unused-variable warning without gssapi feature
285    negotiate_accept_structural(cred, token, channel_binding)
286}
287
288/// Cryptographic GSSAPI verification via `gss_accept_sec_context`.
289///
290/// The `cred` parameter is expected to be a `libgssapi::credential::Cred`
291/// stored inside `Arc<dyn Any>`.  We clone it (reference-counted handle)
292/// for each context establishment.
293///
294/// Returns the authenticated client principal and an optional output token
295/// for mutual authentication.
296#[cfg(feature = "gssapi")]
297fn verify_gssapi_token(
298    cred: &dyn std::any::Any,
299    token: &[u8],
300    channel_binding: Option<&[u8]>,
301) -> Result<NegotiateSuccess, NegotiateError> {
302    use libgssapi::context::{SecurityContext, ServerCtx};
303    use libgssapi::credential::Cred;
304
305    // Downcast to the Cred stored inside Arc<dyn Any>.
306    // main.rs does Arc::new(cred) which coerces Arc<Cred> → Arc<dyn Any>;
307    // .as_ref() on Arc<dyn Any> yields &dyn Any whose inner type is Cred.
308    let gss_cred = cred.downcast_ref::<Cred>().ok_or_else(|| {
309        NegotiateError::Failed(format!(
310            "internal error: GSS credential has wrong type (expected libgssapi::Cred, got TypeId {:?})",
311            cred.type_id(),
312        ))
313    })?;
314
315    // Clone the credential (Cred is Arc<CredInner> — cheap ref count bump).
316    let cred_clone = gss_cred.clone();
317
318    let mut ctx = ServerCtx::new(Some(cred_clone));
319
320    // Process the client's token.  `step()` returns `Some(tok)` when a
321    // response token must be sent back (mutual auth), or `None` when the
322    // context is fully established from the server's perspective.
323    let out_buf = ctx
324        .step(token, channel_binding)
325        .map_err(|e| NegotiateError::Failed(format!("gss_accept_sec_context failed: {e}")))?;
326
327    if ctx.is_complete() {
328        let flags = ctx.flags().map_err(|e| {
329            NegotiateError::Failed(format!("failed to retrieve GSS context flags: {e}"))
330        })?;
331        require_negotiated_channel_binding(channel_binding, flags.bits())
332            .map_err(NegotiateError::Failed)?;
333        // Context established — extract the authenticated client principal.
334        let client_name = ctx.source_name().map_err(|e| {
335            NegotiateError::Failed(format!("failed to retrieve client principal: {e}"))
336        })?;
337
338        let principal = client_name.to_string();
339        let out_token = out_buf.map(|b| b.to_vec()).unwrap_or_default();
340
341        debug!(
342            principal = %principal,
343            has_output_token = !out_token.is_empty(),
344            "GSSAPI cryptographic verification succeeded"
345        );
346
347        Ok(NegotiateSuccess {
348            principal,
349            out_token,
350        })
351    } else {
352        // Multi-leg negotiation: return the continuation token.
353        let out_token = out_buf.map(|b| b.to_vec()).unwrap_or_default();
354
355        if out_token.is_empty() {
356            return Err(NegotiateError::Failed(
357                "GSS context incomplete but no output token produced".into(),
358            ));
359        }
360
361        Err(NegotiateError::Continue(out_token))
362    }
363}
364
365/// Structural-only SPNEGO parsing (no cryptographic verification).
366///
367/// Extracts the service principal name (sname) from the cleartext portion
368/// of the Kerberos ticket.  The client principal is inside the encrypted
369/// ticket data and cannot be retrieved without a keytab.
370///
371/// **Security warning:** The returned identity is prefixed with
372/// `krb5-sname:` to indicate it is NOT a verified client identity.
373fn negotiate_accept_structural(
374    _cred: &dyn std::any::Any,
375    token: &[u8],
376    channel_binding: Option<&[u8]>,
377) -> Result<NegotiateSuccess, NegotiateError> {
378    use synta_krb5::gss;
379
380    warn!(
381        "GSSAPI structural parsing only: Kerberos tickets are NOT cryptographically verified. \
382         The extracted identity is the service name (sname) from the cleartext portion of the \
383         ticket, not the authenticated client principal.  The client principal is inside the \
384         encrypted ticket data and requires the `gssapi` feature with a valid keytab to decrypt."
385    );
386
387    let _ = channel_binding;
388
389    // Step 1: Try parsing as a raw Kerberos GSS token (APPLICATION 0
390    // with KRB5 OID).  Some clients send this directly rather than
391    // wrapping in SPNEGO.
392    if let Some(principal) = try_extract_principal_from_gss_token(token) {
393        debug!(principal = %principal, "extracted principal from raw KRB5 GSS token");
394        return Ok(NegotiateSuccess {
395            principal,
396            out_token: Vec::new(),
397        });
398    }
399
400    // Step 2: Parse as SPNEGO (APPLICATION 0 with SPNEGO OID).
401    // The outer APPLICATION 0 envelope wraps a NegTokenInit whose
402    // mechToken field contains the Kerberos GSS token.
403    if let Some((oid, _tok_type, inner)) = gss::parse_initial_context_token(token) {
404        // Check for SPNEGO OID: 1.3.6.1.5.5.2
405        const SPNEGO_OID_BYTES: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
406        if oid == SPNEGO_OID_BYTES {
407            // The inner bytes are the NegotiationToken (a CHOICE type).
408            // NegTokenInit is [0] IMPLICIT SEQUENCE, mechToken is [2] OCTET STRING.
409            if let Some(mech_token) = extract_spnego_mech_token(inner)
410                && let Some(principal) = try_extract_principal_from_gss_token(&mech_token)
411            {
412                debug!(
413                    principal = %principal,
414                    "extracted principal from SPNEGO mechToken"
415                );
416                return Ok(NegotiateSuccess {
417                    principal,
418                    out_token: Vec::new(),
419                });
420            }
421        }
422
423        // Check for raw KRB5 OID — client may have sent KRB5 token directly
424        // in the APPLICATION 0 envelope.
425        if oid == gss::KRB5_OID_BYTES {
426            // inner is the AP-REQ payload after the 2-byte token type
427            if let Some(principal) = try_extract_principal_from_ap_req(inner) {
428                debug!(
429                    principal = %principal,
430                    "extracted principal from KRB5 APPLICATION 0 token"
431                );
432                return Ok(NegotiateSuccess {
433                    principal,
434                    out_token: Vec::new(),
435                });
436            }
437        }
438    }
439
440    // Step 3: Last-resort attempt — try direct AP-REQ DER decode
441    // (some broken clients send bare AP-REQ without the GSS envelope).
442    if let Some(principal) = try_extract_principal_from_ap_req(token) {
443        debug!(
444            principal = %principal,
445            "extracted principal from bare AP-REQ (non-standard)"
446        );
447        return Ok(NegotiateSuccess {
448            principal,
449            out_token: Vec::new(),
450        });
451    }
452
453    Err(NegotiateError::Failed(
454        "failed to parse SPNEGO/Kerberos token: unable to extract client principal".to_string(),
455    ))
456}
457
458/// Try to parse a GSS APPLICATION 0 Kerberos token and extract the principal.
459fn try_extract_principal_from_gss_token(token: &[u8]) -> Option<String> {
460    use synta_krb5::gss;
461
462    let (oid, tok_type, payload) = gss::parse_initial_context_token(token)?;
463
464    // Must be Kerberos 5 OID and AP-REQ token type.
465    if oid != gss::KRB5_OID_BYTES || tok_type != gss::TOK_AP_REQ {
466        return None;
467    }
468
469    try_extract_principal_from_ap_req(payload)
470}
471
472/// Try to parse an AP-REQ DER blob and extract the service principal name
473/// (sname) from the ticket's cleartext fields.
474///
475/// **Important:** The returned identity is the *service* principal (sname)
476/// from the unencrypted portion of the ticket, NOT the authenticated client
477/// principal.  The client principal is inside the encrypted ticket data and
478/// requires libgssapi with a valid keytab to decrypt.
479///
480/// The returned string is prefixed with `krb5-sname:` to make it clear to
481/// callers that this is not a verified client identity.  For example:
482/// `krb5-sname:HTTP/host.example.com@EXAMPLE.COM`.
483///
484/// For full client identity extraction, the ticket must be decrypted with
485/// the server's keytab — that requires the system GSS-API library.
486fn try_extract_principal_from_ap_req(ap_req_der: &[u8]) -> Option<String> {
487    use synta::{Decoder, Encoding};
488    use synta_krb5::kerberos_v5::ApReq;
489    use synta_krb5::principal::PrincipalNameExt;
490
491    let ap_req: ApReq = Decoder::new(ap_req_der, Encoding::Der).decode().ok()?;
492
493    // Extract the service principal from the ticket (cleartext sname field).
494    let ticket = &ap_req.ticket;
495    let realm = &ticket.realm;
496    let sname = &ticket.sname;
497
498    // Prefix with "krb5-sname:" to indicate this is the service name from
499    // structural parsing, not a cryptographically verified client identity.
500    Some(format!("krb5-sname:{}", sname.display(Some(realm))))
501}
502
503/// Extract the mechToken from an SPNEGO NegTokenInit.
504///
505/// SPNEGO NegTokenInit (RFC 4178 §4.2.1):
506/// ```text
507/// NegTokenInit ::= SEQUENCE {
508///     mechTypes       [0] MechTypeList,
509///     reqFlags        [1] ContextFlags OPTIONAL,
510///     mechToken       [2] OCTET STRING OPTIONAL,
511///     mechListMIC     [3] OCTET STRING OPTIONAL,
512/// }
513/// ```
514///
515/// The `inner` bytes are the IMPLICIT [0] tagged NegTokenInit SEQUENCE
516/// (the NegotiationToken CHOICE tag has already been consumed by the
517/// caller).
518fn extract_spnego_mech_token(inner: &[u8]) -> Option<Vec<u8>> {
519    // NegotiationToken is a CHOICE:
520    //   negTokenInit [0] NegTokenInit
521    //   negTokenResp [1] NegTokenResp
522    //
523    // We expect [0] (context tag 0, constructed).
524    if inner.is_empty() {
525        return None;
526    }
527
528    let tag = inner[0];
529    // Context tag [0] constructed = 0xa0
530    if tag != 0xa0 {
531        return None;
532    }
533
534    // Read length of the [0] wrapper
535    let (wrapper_len, len_bytes) = read_der_length(inner, 1)?;
536    let seq_start = 1 + len_bytes;
537    let seq_end = seq_start + wrapper_len;
538    if seq_end > inner.len() {
539        return None;
540    }
541    let seq_bytes = &inner[seq_start..seq_end];
542
543    // The NegTokenInit SEQUENCE tag
544    if seq_bytes.is_empty() || seq_bytes[0] != 0x30 {
545        return None;
546    }
547    let (seq_len, seq_len_bytes) = read_der_length(seq_bytes, 1)?;
548    let fields_start = 1 + seq_len_bytes;
549    let fields_end = fields_start + seq_len;
550    if fields_end > seq_bytes.len() {
551        return None;
552    }
553    let mut pos = fields_start;
554
555    // Walk through the context-tagged fields looking for [2] mechToken.
556    while pos < fields_end {
557        if pos >= seq_bytes.len() {
558            break;
559        }
560        let field_tag = seq_bytes[pos];
561        let (field_len, field_len_bytes) = read_der_length(seq_bytes, pos + 1)?;
562        let field_value_start = pos + 1 + field_len_bytes;
563        let field_value_end = field_value_start + field_len;
564
565        if field_value_end > seq_bytes.len() {
566            break;
567        }
568
569        // Context tag [2] = 0xa2 (constructed)
570        if field_tag == 0xa2 {
571            // The value is an OCTET STRING wrapping the mechToken
572            let octet_bytes = &seq_bytes[field_value_start..field_value_end];
573            // Strip the OCTET STRING tag (0x04) and length
574            if !octet_bytes.is_empty() && octet_bytes[0] == 0x04 {
575                let (oct_len, oct_len_bytes) = read_der_length(octet_bytes, 1)?;
576                let value_start = 1 + oct_len_bytes;
577                let value_end = value_start + oct_len;
578                if value_end <= octet_bytes.len() {
579                    return Some(octet_bytes[value_start..value_end].to_vec());
580                }
581            }
582            return None;
583        }
584
585        pos = field_value_end;
586    }
587
588    None
589}
590
591/// Read a DER/BER definite-length starting at `bytes[offset]`.
592/// Returns `(value_length, bytes_consumed)` or `None`.
593fn read_der_length(bytes: &[u8], offset: usize) -> Option<(usize, usize)> {
594    let first = *bytes.get(offset)?;
595    if first < 0x80 {
596        Some((first as usize, 1))
597    } else {
598        let n = (first & 0x7f) as usize;
599        if n == 0 || n > 4 || offset + 1 + n > bytes.len() {
600            return None;
601        }
602        let mut val = 0usize;
603        for i in 0..n {
604            val = (val << 8) | bytes[offset + 1 + i] as usize;
605        }
606        Some((val, 1 + n))
607    }
608}
609
610/// RFC5929 tls-server-end-point: hash the serving certificate using its
611/// signature hash, upgrading legacy MD5/SHA1 to SHA256.
612pub fn server_end_point(der: &[u8]) -> Result<Vec<u8>, String> {
613    use synta_certificate::DataHasher;
614    let cert = synta_certificate::Certificate::from_der(der).map_err(|e| e.to_string())?;
615    let oid = cert.signature_algorithm.algorithm.to_string();
616    let hash = match oid.as_str() {
617        "1.2.840.113549.1.1.12" | "1.2.840.10045.4.3.3" => "sha384",
618        "1.2.840.113549.1.1.13" | "1.2.840.10045.4.3.4" => "sha512",
619        "1.2.840.113549.1.1.4"
620        | "1.2.840.113549.1.1.5"
621        | "1.2.840.113549.1.1.11"
622        | "1.2.840.10045.4.1"
623        | "1.2.840.10045.4.3.2" => "sha256",
624        _ => {
625            return Err(format!(
626                "tls-server-end-point unsupported certificate signature algorithm {oid}"
627            ));
628        }
629    };
630    synta_certificate::default_data_hasher()
631        .hash_data(hash, der)
632        .map_err(|e| e.to_string())
633}
634
635/// libgssapi retains unknown flag bits but does not name CHANNEL_BOUND yet.
636/// GSS_C_CHANNEL_BOUND_FLAG is 2048 (MIT Kerberos GSS extension).
637#[cfg(any(feature = "gssapi", test))]
638fn require_negotiated_channel_binding(binding: Option<&[u8]>, flags: u32) -> Result<(), String> {
639    const CHANNEL_BOUND: u32 = 0x800;
640    if binding.is_none_or(|b| b.is_empty()) || flags & CHANNEL_BOUND == 0 {
641        return Err("GSSAPI context did not negotiate TLS channel binding".into());
642    }
643    Ok(())
644}
645
646#[cfg(test)]
647mod channel_binding_tests {
648    use super::require_negotiated_channel_binding;
649
650    #[test]
651    fn server_binding_alone_does_not_authenticate_unbound_context() {
652        assert!(require_negotiated_channel_binding(Some(&[1; 32]), 0).is_err());
653        // Other flags (mutual authentication, integrity, confidentiality) do not suffice.
654        assert!(require_negotiated_channel_binding(Some(&[1; 32]), 0x3e).is_err());
655        assert!(require_negotiated_channel_binding(None, 0x800).is_err());
656        assert!(require_negotiated_channel_binding(Some(&[]), 0x800).is_err());
657        assert!(require_negotiated_channel_binding(Some(&[1; 32]), 0x83e).is_ok());
658    }
659}