1use 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#[derive(Clone)]
35pub struct NegotiateOutToken(pub HeaderValue);
36
37#[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#[derive(Clone)]
61pub struct TlsChannelBinding(pub Vec<u8>);
62
63pub 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 let token_b64 = auth_header.strip_prefix("Negotiate ")?;
77
78 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 #[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 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 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 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 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 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 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
215pub 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
226struct NegotiateSuccess {
229 principal: String,
230 out_token: Vec<u8>,
231}
232
233#[allow(dead_code)]
234enum NegotiateError {
235 Continue(Vec<u8>),
237 Failed(String),
239}
240
241fn 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 #[cfg(feature = "gssapi")]
279 if require_crypto {
280 return verify_gssapi_token(cred, token, channel_binding);
281 }
282
283 let _ = require_crypto; negotiate_accept_structural(cred, token, channel_binding)
286}
287
288#[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 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 let cred_clone = gss_cred.clone();
317
318 let mut ctx = ServerCtx::new(Some(cred_clone));
319
320 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 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 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
365fn 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 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 if let Some((oid, _tok_type, inner)) = gss::parse_initial_context_token(token) {
404 const SPNEGO_OID_BYTES: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
406 if oid == SPNEGO_OID_BYTES {
407 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 if oid == gss::KRB5_OID_BYTES {
426 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 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
458fn 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 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
472fn 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 let ticket = &ap_req.ticket;
495 let realm = &ticket.realm;
496 let sname = &ticket.sname;
497
498 Some(format!("krb5-sname:{}", sname.display(Some(realm))))
501}
502
503fn extract_spnego_mech_token(inner: &[u8]) -> Option<Vec<u8>> {
519 if inner.is_empty() {
525 return None;
526 }
527
528 let tag = inner[0];
529 if tag != 0xa0 {
531 return None;
532 }
533
534 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 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 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 if field_tag == 0xa2 {
571 let octet_bytes = &seq_bytes[field_value_start..field_value_end];
573 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
591fn 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
610pub 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#[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 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}