Devolutions / Devolutions/sspi-rs

NTLM-only authentication broken in 0.18.8+ when USE_SESSION_KEY is requested without Kerberos

Open
#640 19 comments 1 reaction 1 assignee Claimed by @TheBestTvarynka View on GitHub
Dominant language
Rust
Stars
83
Forks
50
Avg merge
3d 12h
Merged PRs (30d)
9

Description

# NTLM-only authentication broken in sspi 0.18.8+ when `USE_SESSION_KEY` is requested without Kerberos

## Summary

Starting in sspi 0.18.8 (PR #600), consumers that request `ClientRequestFlags::USE_SESSION_KEY` without providing a target SPN get a hard error during `initialize_security_context`. In 0.18.7, this worked fine — NTLM negotiation proceeded without requiring an SPN. This is a breaking change that affects any SMB client using NTLM-only authentication.

Additionally, the NTLM-on-IP-address fallback added in PR #637 (sspi 0.18.9) does not fully work — the Kerberos TGT-REQ token is still generated even after the protocol is downgraded to NTLM, causing `STATUS_LOGON_FAILURE` on non-domain-joined servers.

## Environment

- **sspi versions tested:** 0.18.7 (works), 0.18.8 (broken), 0.18.9 (broken)
- **SMB client:** [smb-rs](https://github.com/AvivNaaman/smb-rs) v0.11.1, built without `kerberos` feature
- **Server:** Dell PowerScale (Isilon) OneFS — local user authentication, no Active Directory/Kerberos domain
- **Auth method:** NTLM only (`auth_methods: { ntlm: true, kerberos: false }`)
- **Connection target:** IP address (DNS resolved before SMB connect, e.g., `//10.120.4.12/share`)

## Reproduction

### Minimal setup

Any SMB client using sspi's `Negotiate` SSP with:
1. `ClientRequestFlags::USE_SESSION_KEY` in context requirements
2. No `target_name` provided (or `target_name` provided as an IP-based SPN like `cifs/10.120.4.12`)
3. NTLM-only package list (no Kerberos)
4. Server is a non-domain-joined NAS (OneFS, Samba, etc.) with local user accounts

### Test matrix (verified)

All tests: same smb-rs 0.11.1, same server, same credentials, same dataset (53 files), fresh SMB sessions each time.

| Test | sspi version | SPN provided | Result |
|------|-------------|-------------|--------|
| 1 | **0.18.7** | No | ✅ **53/53 success** |
| 2 | **0.18.8** | No | ❌ 0/53 — `NoCredentials: Service target name (service principal name) is not provided` |
| 3 | **0.18.9** | No | ❌ 0/53 — Same `NoCredentials` error |
| 4 | **0.18.9** | Yes (`cifs/10.120.4.12`) | ❌ 0/53 — `Logon Failure (0xc000006d)` |

## Bug 1: `USE_SESSION_KEY` requires SPN even for NTLM (0.18.8+)

### Location

`src/negotiate/client.rs:70-82` (introduced in PR #600):

```rust
match negotiate.state {
NegotiateState::Initial => {
let sname = if builder
.context_requirements
.contains(ClientRequestFlags::USE_SESSION_KEY)
{
let (service_name, service_principal_name) =
parse_target_name(builder.target_name.ok_or_else(|| {
Error::new(
ErrorKind::NoCredentials,
"Service target name (service principal name) is not provided",
)
})?)?;
Some([service_name, service_principal_name])
} else {
None
};
```

### Problem

`USE_SESSION_KEY` unconditionally requires `target_name` to be `Some`. This is incorrect for NTLM — NTLM always derives a session key during authentication regardless of whether `USE_SESSION_KEY` is requested. The flag is only meaningful for Kerberos, where the session key comes from the service ticket.

### Previous behavior (0.18.7)

In `src/negotiate.rs:600+`, the `initialize_security_context` function did not check for `USE_SESSION_KEY` at all. It processed `target_name` only for NTLM downgrade detection (IP address check) and proceeded directly to protocol negotiation.

### Impact

Any consumer that:
- Requests `USE_SESSION_KEY` (common — smb-rs always does)
- Doesn't provide a `target_name` (because Kerberos isn't being used)
- Uses NTLM-only authentication

...gets a hard `NoCredentials` error on every session setup.

## Bug 2: NTLM-on-IP downgrade still generates Kerberos TGT-REQ token (0.18.9)

### Location

`src/negotiate/client.rs:68-100` and `src/negotiate/generators.rs:48-80`

### Problem

When a consumer provides an IP-based SPN (e.g., `cifs/10.120.4.12`), the flow is:

1. `check_target_name_for_ntlm_downgrade` (called before `NegotiateState::Initial`) detects IP → sets `negotiate.protocol = NegotiatedProtocol::Ntlm(...)` ✅
2. `NegotiateState::Initial` → `USE_SESSION_KEY` check → parses SPN into `sname = Some(["cifs", "10.120.4.12"])`
3. `generate_mech_type_list(matches!(protocol, Kerberos(_)), ntlm)` → correctly uses NTLM mech types (because protocol was downgraded) ✅
4. `generate_neg_token_init(sname, mech_types)` → **generates a Kerberos TGT-REQ token** because `sname` is `Some` ❌

In `generators.rs:52-72`, when `sname` is `Some`, a full Kerberos `TgtReq` message is constructed:
```rust
let krb5_neg_token_init = ApplicationTag0(KrbMessage {
krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()),
krb5_token_id: TGT_REQ_TOKEN_ID,
krb_msg: TgtReq {
server_name: PrincipalName { name_string: sname },
},
});
```

This Kerberos token is embedded in the SPNego `NegTokenInit.mech_token` field. The server receives an SPNego init that says "I support NTLM" in `mech_types` but also contains a Kerberos TGT-REQ in `mech_token`. Non-domain-joined servers (OneFS, Samba with local users) attempt to process the Kerberos token, fail, and return `STATUS_LOGON_FAILURE (0xc000006d)`.

### Expected behavior

When the protocol has been downgraded to NTLM (by the IP address check or any other reason), `sname` should be set to `None` so that `generate_neg_token_init` produces a clean NTLM-only SPNego init without a Kerberos mech_token.

## Suggested fix

In `src/negotiate/client.rs`, after the `USE_SESSION_KEY` → `sname` extraction, check if the protocol is NTLM and clear the sname:

```rust
let sname = if builder
.context_requirements
.contains(ClientRequestFlags::USE_SESSION_KEY)
{
// ... existing SPN parsing ...
Some([service_name, service_principal_name])
} else {
None
};

// Don't include Kerberos TGT-REQ when protocol has been downgraded to NTLM
let sname = if matches!(&negotiate.protocol, NegotiatedProtocol::Ntlm(_)) {
None
} else {
sname
};
```

Or alternatively, don't require `target_name` when the negotiated protocol is NTLM — since NTLM doesn't use the SPN for session key derivation, `USE_SESSION_KEY` should not gate on target_name for NTLM.

## Workaround

Pin `sspi = "=0.18.7"` in `Cargo.toml`. This is the last version where NTLM-only authentication works without providing an SPN.

## Related

- PR #600 — introduced the `USE_SESSION_KEY` → SPN requirement (breaking change)
- PR #637 — added NTLM-on-IP fallback (incomplete — doesn't clear sname)
- Issue #620 — related regression from same PR #600 (FreeRDP Kerberos fallback failure, different code path)
- [smb-rs](https://github.com/AvivNaaman/smb-rs) — affected consumer (always requests `USE_SESSION_KEY`, only provides SPN with `kerberos` feature)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.