Skip to content

fix(kerberos): enforce the sequence number of MIC token only for acce… - #772

Merged
Marc-André Moreau (mamoreau-devolutions) merged 4 commits into
Devolutions:masterfrom
Rostyslav-Romanets:fix/kerberos-reject-mech-list-mic
Oct 19, 2026
Merged

Marc-André Moreau (mamoreau-devolutions) merged 4 commits into
Devolutions:masterfrom
Rostyslav-Romanets:fix/kerberos-reject-mech-list-mic

Conversation

@Rostyslav-Romanets

@Rostyslav-Romanets Rostyslav-Romanets commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

This PR fixes #748 by enforcing sequence number verification only on the acceptor side. The initiator verifies the MIC checksum without enforcing the sequence number.

@Rostyslav-Romanets
Rostyslav-Romanets marked this pull request as ready for review October 11, 2026 17:14
Copilot AI balanced review requested due to automatic review settings October 11, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The shared validator unintentionally disables PKU2U client sequence checks and lacks regression coverage.

3 open findings
What changed in this PR

Restores Windows Kerberos interoperability by skipping initiator-side MIC sequence validation.

Changes:

  • Limits sequence-number enforcement to acceptors.
  • Documents the interoperability rationale.
File Description
src/​kerberos/​utils.rs Changes MIC sequence validation behavior.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/kerberos/utils.rs Resolved
Comment thread src/kerberos/utils.rs Resolved
Comment thread src/kerberos/utils.rs Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation addresses the interoperability regression while retaining sequence enforcement in the intended paths.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

@Rostyslav-Romanets

Copy link
Copy Markdown
Contributor Author

The 2 open findings have been resolved, but Copilot hasn't closed them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mamoreau-devolutions
Marc-André Moreau (mamoreau-devolutions) merged commit 08d8622 into Devolutions:master Oct 19, 2026
68 checks passed
Marc-André Moreau (mamoreau-devolutions) added a commit that referenced this pull request Oct 23, 2026
## Summary

Follow up on [#768](#768),
based on freshly fetched `origin/master` at
`cf91195338f62a8b01ca5024e06f97c150e03225`, including that merged PR.

- Carry the negotiated preauthentication salt out of the AS exchange for
password changes and password-backed U2U acceptor TGT acquisition, using
the same reply-salt selection helper as the direct/IAKerb client.
- Distinguish an absent PA-ETYPE-INFO2 hint, an omitted salt field, and
an explicitly empty salt. Retain the existing public extractor
signatures.
- Share authenticated AS-REP decryption between password/keytab and the
actual PKINIT-facing PKU2U extractors, accepting application tags 25 and
26 without weakening decryption or nonce handling.
- Add 27 deterministic regressions, reusing the KDC mocks and
direct/IAKerb flows. Fix the reported test-target Clippy findings and
the RFC section-title capitalization.

## Salt semantics

RFC 4120 section 4, “Encryption and Checksum Specifications”, defines
the default salt as the realm followed by principal components without
separators; section 5.2.7.5, “PA-ETYPE-INFO2”, defines the optional salt
field.

| Reply information | Salt used |
| --- | --- |
| PA-ETYPE-INFO2 absent | Salt used for preauthentication |
| Entry present, salt field absent | Default from the reply client realm
and principal components |
| Salt explicitly empty | Empty salt, even when preauthentication used a
different salt |
| Salt explicitly nonempty | Reply-provided salt, taking precedence over
preauthentication |

The preauthentication path also distinguishes an absent hint from a
present entry requesting the principal's default salt. Existing
explicit-salt/tag-25 behavior and the IAKerb state machine are retained.

## Requirement coverage

| Requirement | Deterministic evidence |
| --- | --- |
| Password-change salt fallback |
`kerberos_change_password_uses_negotiated_salt_without_as_rep_hint`
invokes the actual password-change consumer and decrypts/asserts the
outgoing ticket, authenticator, and exact new-password bytes |
| Password-backed U2U acceptor TGT acquisition |
`kerberos_u2u_server_acquires_tgt_with_password_without_as_rep_hint`
starts without a preinstalled ticket-decryption key, acquires the
acceptor TGT, and completes authentication plus encrypted application
traffic |
| Combined missing reply hint and tag 26 |
`kerberos_auth_without_as_rep_etype_info2_with_tgs_rep_enc_part_in_as_rep`
|
| The same combination through IAKerb |
`iakerb_auth_without_as_rep_etype_info2_with_tgs_rep_enc_part_in_as_rep`,
including cookies/transcript and encrypted application traffic |
| Explicit-empty and custom reply salts override a different preauth
salt | `kerberos_auth_with_explicit_empty_as_rep_salt`,
`kerberos_auth_with_custom_as_rep_salt`,
`extract_encryption_params_from_as_rep_with_salt_preserves_explicit_reply_salt`
|
| Omitted reply salt field uses the reply-principal default |
`kerberos_auth_with_missing_as_rep_salt_field`,
`extract_encryption_params_from_as_rep_with_salt_defaults_missing_reply_salt_field`
assert case-preserving, separator-free realm/multicomponent-principal
concatenation |
| Missing versus empty preauth salt |
`extract_preauth_salt_distinguishes_absent_default_empty_and_custom`,
`kerberos_auth_with_missing_preauth_salt_field`,
`kerberos_auth_with_explicit_empty_preauth_salt` |
| Password/keytab and shared decoder tag compatibility |
`kerberos_password_extractor_returns_expected_session_key_for_both_application_tags`,
`kerberos_derived_key_extractor_returns_expected_session_key_for_both_application_tags`,
`decrypt_as_rep_preserves_fields_for_both_application_tags` |
| Actual PKINIT-facing key-only and key+nonce decoding |
`pku2u_public_key_extractor_returns_expected_session_key_for_both_application_tags`,
`pku2u_key_and_nonce_extractor_preserves_nonce_boundaries` |
| Invalid tags, wrong keys, tampering, and nonce overflow still fail |
`kerberos_session_key_extractors_reject_unsupported_application_tag`,
`pku2u_session_key_extractors_reject_unsupported_application_tag`,
`kerberos_session_key_extractors_reject_wrong_key_and_tampered_ciphertext`,
`pku2u_session_key_extractors_reject_wrong_key_and_tampered_ciphertext`,
`pku2u_nonce_extractor_rejects_nonce_above_u32_max` |
| Public compatibility wrappers and malformed metadata |
`extract_salt_from_krb_error_preserves_legacy_optional_string_contract`,
`extract_encryption_params_from_as_rep_preserves_one_argument_api`,
`salt_extractors_reject_invalid_challenges`,
`reply_encryption_params_reject_malformed_or_missing_values` |
| Cleanup | RFC title is `KRB_KDC_REP definition`; checked IAKerb
`usize::try_from` conversion and structured trace logging in the common
test helper; strict Clippy passes |

## Validation

Local Windows validation with VsDevShell initialized in each build
process; `AWS_LC_SYS_NO_ASM=true`, `CMAKE_GENERATOR=Ninja`, and
`SSPI_RS_IS_RUNNING_TESTS=true` for tests:

- `cargo test --locked -q -p sspi --lib --features
network_client,dns_resolver,scard,tsssp,__test-data`: **307 passed, 0
failed**.
- `cargo test --locked -q -p sspi --test sspi --features
network_client,dns_resolver,scard,tsssp,__test-data
client_server::kerberos:: -- --test-threads=1`: **22 passed, 0 failed**.
- Scoped extractor runs: **16 Kerberos tests** and **7 PKU2U tests**
passed. The affected password-change and U2U tests were also rerun after
the final Clippy cleanup.
- `cargo clippy --locked -q -p sspi --lib --test sspi --features
network_client,dns_resolver,scard,tsssp,__test-data -- -D warnings -D
clippy::print_stdout`: passed.
- `cargo check --locked -q -p sspi --no-default-features`: passed.
- `cargo fmt --all -- --check` and `git diff --check`: passed; style
review passed.
- Standalone `tools/wasm-testcompile` fresh dependency resolution
selected **picky-krb 0.13.0**; both `cargo check` and `cargo build` with
`--locked --target wasm32-unknown-unknown` passed. The full `check.ps1`
import scan was not run (`wasm2wat` is unavailable).

Formatting emits the existing stable-rustfmt warnings for nightly-only
options. Cargo emits nonfatal incremental-cache access-denied notes;
WASM compilation also emits the existing `encode_digest` dead-code
warning.

## Investigated without changes

- **WASM dependency failure:** current master already requires
`picky-krb = "0.13"` (raised in #772), which excludes the older 0.12.4
release lacking `InvalidMechanismOid`. Fresh resolution and an actual
standalone wasm32 build succeeded. No additional pin, lockfile change,
or external workspace dependency is warranted.
- **Dependency-fetch hang:** the reported historical macOS winscard
timeout remains an infrastructure symptom, not a reproduced source
failure. The historical failed-run jobs API returned HTTP 502, so its
detailed logs could not be revalidated here. The later run
[39320794389](https://github.com/Devolutions/sspi-rs/actions/runs/39320794389)
has successful WASM and Windows/macOS/Linux winscard jobs; the
RustCrypto/KDFs dependency is already revision-locked. No speculative
timeout/retry infrastructure or dependency update was added.

No live Windows/MIT/Heimdal KDC validation was performed. Explicit-empty
reply salt is a theoretical deterministic case, not a claimed real-world
reproduction. Password-change coverage verifies the encrypted outgoing
request, not a completed password update on a KDC.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Samer Aburabie (SamerX) pushed a commit to SamerX/Kubexer-IronRDP that referenced this pull request Oct 27, 2026
Kerberos (CredSSP) sign-in from the web client fails against Windows with:

  CredSSP: InvalidToken: invalid MIC token sequence number: expected <n> but got 0

sspi 0.23.0 validates the sequence number of the SPNEGO mechListMIC on the
initiator side, but the Windows acceptor signs it with sequence number 0
regardless of the AP-REP sequence number. sspi 0.21.0 (used by the upstream
npm 0.7.0 package) did not have this check.

Upstream issue: Devolutions/sspi-rs#748
Upstream fix:   Devolutions/sspi-rs#772 (merge commit 08d86227, 2026-10-19),
                which enforces the sequence number only on the acceptor side.

The fix is not released on crates.io yet (latest is 0.23.0), so patch sspi to
that git rev via [patch.crates-io]. The rev is 0.23.0 plus five commits:
picky-* dependency bump (picky-krb 0.12 -> 0.13, locked to 0.13.0 as upstream
does; 0.13.1 does not build with this sspi), a `.\` local-qualifier username
parsing fix (Devolutions#767), FFI tests, agent/style docs, and the MIC fix itself.
winscard comes from the same git rev (sspi workspace member, docs-only diff).

Drop the patch once an sspi release newer than 0.23.0 is on crates.io.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Kerberos client rejects the server's SPNEGO mechListMIC since 0.22.0: "invalid MIC token sequence number: expected N but got 0"

3 participants