Skip to content

feat: add keytab client credentials and standards-compliant SASL/GSSAPI acceptor support#681

Merged
Benoît Cortier (CBenoit) merged 4 commits into
Devolutions:masterfrom
robot-head:crabka/sasl-gssapi
Jun 17, 2026
Merged

feat: add keytab client credentials and standards-compliant SASL/GSSAPI acceptor support#681
Benoît Cortier (CBenoit) merged 4 commits into
Devolutions:masterfrom
robot-head:crabka/sasl-gssapi

Conversation

@robot-head

Copy link
Copy Markdown
Contributor

Summary

This PR adds the pieces needed for sspi to interoperate as both initiator and acceptor with standard SASL/GSSAPI (RFC 4752) Kerberos peers — most notably mainstream clients like the JDK/Apache Kafka GSSAPI stack. It contains three independent, additive capabilities:

  1. Keytab-based client credentials — authenticate as a Kerberos client using a pre-derived long-term key (as stored in a keytab) instead of a password.
  2. Integrity-only (unsealed) Wrap token decryption — accept GSS_Wrap tokens produced with conf_req_flag == FALSE (RFC 4121 §4.2.4), which standard clients use for the RFC 4752 security-layer negotiation.
  3. Multiple service principals per acceptor — let one acceptor validate an incoming AP-REQ against any of several configured SPNs, matching how MIT/Heimdal GSSAPI acceptors key off a whole keytab.

Each is usable on its own; together they enable a full SASL/GSSAPI handshake against stock clients.

Motivation

A service acting on behalf of a machine identity has no password — it has a keytab. And real-world SASL/GSSAPI clients (e.g. the JDK GSSAPI provider used by Apache Kafka) negotiate the security layer with an unsealed Wrap token and frequently obtain tickets for one of several host SPNs held in a single keytab. Today sspi:

  • has no credential variant for a raw long-term key (only password and smart card),
  • rejects any Wrap token without the Sealed flag ("the Sealed flag has to be set in WRAP token"), and
  • pins the acceptor to exactly one service name, failing tickets that legitimately name a different SPN from the same keytab.

This PR closes those three gaps.

Changes

1. KeytabIdentity credential (auth_identity.rs, lib.rs)

  • New public KeytabIdentity { principal: Username, key: Secret<Vec<u8>>, key_enctype: u8 }, re-exported from the crate root, plus Credentials::Keytab / CredentialsBuffers::Keytab variants and a From<KeytabIdentity> for Credentials impl.
  • The Kerberos client (kerberos/client/mod.rs) uses the raw key directly: it encrypts the PA-ENC-TIMESTAMP pre-auth value and decrypts the AS-REP with the long-term key, skipping string-to-key derivation (new GenerateKeytabPaDataOptions / generate_pa_datas_for_as_req_with_key in generators.rs, and extract_session_key_from_as_rep_with_key in extractors.rs).
  • Keytab credentials are explicitly rejected with a clear error where they don't apply (NTLM and CredSSP paths in credssp/).

2. Integrity-only Wrap token support (kerberos/mod.rs)

  • When the Sealed flag is clear, instead of erroring, the acceptor now verifies the token via a new decrypt_integrity_only_wrap helper and returns the cleartext payload.
  • Implements RFC 4121 §4.2.4 exactly: undo the RRC right-rotation, split plaintext | checksum, recompute the keyed checksum over plaintext | header (with the EC and RRC header fields zeroed) using the SEAL key usage, and compare. Integrity failures surface as KerberosCryptoError::IntegrityCheck.

3. Multiple acceptor service keys (kerberos/server/mod.rs)

  • ServerProperties gains additional_service_keys: Vec<(PrincipalName, Secret<Vec<u8>>)> and a builder method add_service_key(&mut self, sname: &[&str], key).
  • New ticket_decryption_key_for(&self, ticket_sname) resolves the decryption key by matching the ticket's service name against the primary service_name and the additional keys, comparing name components only (ignoring name-type, per RFC 4120 §6.2).
  • accept_security_context uses this resolver instead of a single hard-coded equality check, so an AP-REQ is accepted as long as its ticket names some configured SPN.

Compatibility / scope

  • Purely additive. No public item is removed or renamed; password and smart-card flows are untouched. The change to CredentialsBuffers::{into,to,as,as_mut}_auth_identity is a non-functional cleanup (the catch-all arm replaces a #[cfg(feature = "scard")]-gated one now that a non-scard variant exists).
  • Based on the sspi-v0.21.0 tag. cargo check passes on default features.

Testing notes / open questions for review

  • The integrity-only Wrap path and the multi-SPN resolver were validated against a live JDK/Kafka SASL/GSSAPI client; I'd welcome guidance on where you'd like unit/integration coverage added (the existing tests/sspi/client_server/kerberos harness seems the natural home).
  • Happy to gate any of the three pieces behind a feature flag or split them into separate PRs if you'd prefer to review them independently.

Port of crabka's local sspi modifications, originally vendored into
third_party/sspi as a `cargo package` snapshot of sspi v0.21.0.

Adds the Kerberos-side support needed for SASL/GSSAPI authentication
across the client and server exchanges (AS-REQ generators/extractors,
ts_request, auth_identity, and CredSSP plumbing).

Rebased onto the upstream sspi-v0.21.0 tag so the changes can be
maintained and rebased against upstream directly. Cargo-packaging
artifacts (normalized Cargo.toml, Cargo.lock, Cargo.toml.orig,
.cargo_vcs_info.json) and renovate/dependabot dep bumps from the
vendored tree are intentionally excluded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matthew Stone (robot-head) added a commit to robot-head/crabka that referenced this pull request May 30, 2026
…#338)

* chore(deps): load sspi fork from git instead of vendoring third_party

Replace the vendored `cargo package` snapshot under third_party/sspi with
a git dependency on our sspi fork branch, wired through `[patch.crates-io]`.
The fork (branched off the sspi-v0.21.0 tag) carries our SASL/GSSAPI
Kerberos work: keytab client credentials, integrity-only Wrap tokens, and
multi-SPN acceptor keys. Upstreaming tracked at Devolutions/sspi-rs#681.

Cargo.lock pins the exact fork commit, so builds stay reproducible despite
the ref being a branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(deny): allow our sspi fork git source

cargo-deny's [sources] denies unknown git sources; allow the sspi fork
URL now that sspi is pulled from git rather than vendored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the sspi Kerberos implementation to interoperate with standards-compliant SASL/GSSAPI (RFC 4752 / RFC 4121) peers by adding keytab-backed client credentials, supporting integrity-only (unsealed) GSS_Wrap tokens, and allowing acceptors to validate tickets for multiple configured service principals.

Changes:

  • Add KeytabIdentity / Credentials::{Keytab,...} to authenticate Kerberos clients using a pre-derived long-term key instead of a password.
  • Accept integrity-only (conf=false) Wrap tokens by validating the checksum and returning the cleartext payload.
  • Allow a Kerberos acceptor to resolve the ticket decryption key across multiple configured SPNs (keytab-like behavior), and improve non-mutual-auth context establishment for SASL flows.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/lib.rs Re-exports KeytabIdentity from the crate root.
src/auth_identity.rs Introduces KeytabIdentity and plumbs Keytab through Credentials / CredentialsBuffers.
src/kerberos/client/mod.rs Enables AS exchange using keytab-based pre-auth and session-key extraction.
src/kerberos/client/generators.rs Adds keytab PA-DATA generation and fixes AS-REQ cname encoding for service principals.
src/kerberos/client/extractors.rs Adds AS-REP session-key extraction using a pre-derived long-term key, with broader tag acceptance.
src/kerberos/pa_datas.rs Adds Keytab variants for PA-DATA generation and AS-REP session-key extraction paths.
src/kerberos/mod.rs Adds integrity-only WRAP verification and routes unsealed WRAP tokens through it.
src/kerberos/server/mod.rs Adds multi-SPN ticket-key resolution and improves non-mutual-auth context establishment for SASL use.
src/kerberos/server/as_exchange.rs Explicitly rejects keytab credentials in server AS-exchange paths where unsupported.
src/credssp/mod.rs Explicitly rejects keytab credentials for NTLM/CredSSP paths.
src/credssp/ts_request/mod.rs Explicitly rejects keytab credentials for CredSSP TS credentials encoding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/kerberos/client/generators.rs Outdated
Comment thread src/kerberos/mod.rs
Comment thread src/kerberos/server/mod.rs Outdated
Comment thread src/kerberos/server/mod.rs Outdated
Comment thread src/kerberos/mod.rs
Comment thread src/kerberos/client/generators.rs
- Stop logging raw long-term/derived key material in the AS timestamp trace.
- Reject integrity-only WRAP tokens for non-AES cipher suites with an
  explicit error instead of silently defaulting to AES-256.
- Store the keytab pre-auth key as Secret<Vec<u8>> to avoid extra
  non-zeroizing copies of long-term secrets.
- Add a unit test covering the integrity-only (unsealed) WRAP path.
- Fix "clint time" typo in the authenticator ctime error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, Matthew Stone (@robot-head)

First of all, sorry for the late review 🥺 🙏
I reviewed your changes. The code is good, and I have a few questions and comments.

The implemented feature is nice and will be useful in the future.

Comment thread src/kerberos/client/extractors.rs Outdated
Comment on lines +333 to +341
// A Kerberos principal name is a sequence of `/`-separated
// components (RFC 1964 §2.1.1). Service principals such as
// `kafka/host` carry two components; user principals carry one.
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(
username
.split('/')
.map(|c| Ok(KerberosStringAsn1::from(IA5String::from_string(c.to_owned())?)))
.collect::<Result<Vec<_>>>()?,
)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        // components (RFC 1964 §2.1.1). Service principals such as
        // `kafka/host` carry two components; user principals carry one.

Question: cname field means client name, not service name. rfc4120#section-5.3:

This field contains the name part of the client's principal identifier.

For example:

Image

The cname field is usually a [NT_PRINCIPAL, plain username] or [NT_ENTERPRISE, fqdn].

Am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In keytab-based auth the client principal is itself a service principal: keytab.principal.account_name() is e.g. kafka/host, a two-component name, and the /-split encodes those components into cname's name-string. For ordinary user principals (no /) the split yields a single component, so the password/smart-card flows are unchanged. So here cname legitimately carries a multi-component client name. I can gate the split on the keytab path specifically if you'd rather keep the shared builder strictly single-component — let me know.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I read more about it. Let's keep this then.

I have one question. I have never tried keytab logon myself. In your case, the cname type will be NT_PRINCIPAL, and the cname value will be a sequence keytab.principal.account_name().split('/'). Does it work well for you? (I am just curious)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does appear to work for us yes

Comment thread src/kerberos/server/mod.rs
Comment thread src/kerberos/server/mod.rs Outdated
Comment thread src/kerberos/server/mod.rs
Comment thread src/kerberos/mod.rs Outdated
Comment thread src/auth_identity.rs Outdated
- Reject EncTGSRepPart in the AS-REP session-key extractor: the AS
  exchange enc-part is EncASRepPart (APPLICATION 25); the TGS tag does
  not belong here.
- Type the keytab key encryption type as CipherSuite instead of a raw
  u8 enctype number on KeytabIdentity / GenerateKeytabPaDataOptions.
- Simplify the integrity-only WRAP checksum header zeroing to a single
  copy_from_slice over the contiguous EC+RRC range.
- Rename `raw` to `seq_number_bytes` in the authenticator sequence
  number decode for clarity.
- Collapse the nested service-name/ticket-key match into a let-chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. (sorry for the late response. I was on vacation)

@TheBestTvarynka

Copy link
Copy Markdown
Collaborator

Benoît Cortier (@CBenoit), I approved the PR. Now it's your turn 🙃

@TheBestTvarynka

Copy link
Copy Markdown
Collaborator

Matthew Stone (@robot-head), I approved the CI. You can fix the CI errors in the meantime

CI's "Check formatting" job runs `cargo fmt --all -- --check` on the
pinned stable toolchain (1.93.0). Four files introduced in this branch
were formatted with nightly-only import rules and did not satisfy stable
rustfmt, failing the job and gating every downstream CI job.

Apply `cargo fmt --all` so the formatting check passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ab5DDjcquWcitoUmWwVvmS

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CBenoit Benoît Cortier (CBenoit) changed the title Add keytab client credentials and standards-compliant SASL/GSSAPI acceptor support feat: add keytab client credentials and standards-compliant SASL/GSSAPI acceptor support Jun 17, 2026
@CBenoit Benoît Cortier (CBenoit) merged commit d95f249 into Devolutions:master Jun 17, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants