Files
markbase/vendor/smb-server/src/proto/auth/spnego.rs
Warren 7eb528d35f
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
SMB Server Phase 2: VFS backend build fix + integration test
- Add VfsFile: Send supertrait for Mutex compatibility
- Fix SmbServerCommand: struct → Subcommand enum with Start variant
- Fix tracing_subscriber::init() → try_init() to avoid panic when
  logger already initialized
- Fix CLI subcommand name: smb-server → smb-start (flatten naming)
- Add #[command(name = "smb-start")] for CLI disambiguation
- Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs)
- Remove unused VfsFile imports (webdav.rs, scp_handler.rs)
- Integration test: Docker smbclient verified (list, upload, read)
2026-06-20 19:42:29 +08:00

525 lines
17 KiB
Rust

//! Minimal hand-rolled DER codec for SPNEGO (MS-SPNG / RFC 4178).
//!
//! v1 advertises **only** the NTLMSSP mechanism. We don't pull in a full
//! ASN.1 crate; this is a tiny subset of DER for the few SPNEGO tokens we
//! need to encode/decode during SESSION_SETUP.
//!
//! ASN.1 sketch:
//!
//! ```text
//! GSSAPI-Token (RFC 2743) ::= [APPLICATION 0] IMPLICIT SEQUENCE {
//! thisMech OBJECT IDENTIFIER, -- SPNEGO 1.3.6.1.5.5.2
//! innerContextToken ANY DEFINED BY thisMech
//! }
//!
//! NegotiationToken ::= CHOICE {
//! negTokenInit [0] NegTokenInit,
//! negTokenResp [1] NegTokenResp
//! }
//!
//! NegTokenInit ::= SEQUENCE {
//! mechTypes [0] MechTypeList,
//! reqFlags [1] ContextFlags OPTIONAL,
//! mechToken [2] OCTET STRING OPTIONAL,
//! mechListMIC [3] OCTET STRING OPTIONAL
//! }
//!
//! NegTokenResp ::= SEQUENCE {
//! negState [0] ENUMERATED OPTIONAL,
//! supportedMech [1] OBJECT IDENTIFIER OPTIONAL,
//! responseToken [2] OCTET STRING OPTIONAL,
//! mechListMIC [3] OCTET STRING OPTIONAL
//! }
//! ```
use crate::proto::error::{ProtoError, ProtoResult};
// --- Universal & well-known tags --------------------------------------------
const TAG_SEQUENCE: u8 = 0x30; // SEQUENCE OF / SEQUENCE (constructed)
const TAG_OBJECT: u8 = 0x06; // OBJECT IDENTIFIER
const TAG_OCTET: u8 = 0x04; // OCTET STRING
const TAG_ENUMERATED: u8 = 0x0a; // ENUMERATED
const TAG_APP_0: u8 = 0x60; // [APPLICATION 0] IMPLICIT — GSS-API outer
const TAG_CTX_0: u8 = 0xa0;
const TAG_CTX_1: u8 = 0xa1;
const TAG_CTX_2: u8 = 0xa2;
const TAG_CTX_3: u8 = 0xa3;
// --- OIDs ------------------------------------------------------------------
/// SPNEGO `1.3.6.1.5.5.2` encoded as the *content* of an OBJECT IDENTIFIER
/// (i.e. **without** the leading 0x06 tag + length).
pub const OID_SPNEGO: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
/// NTLMSSP `1.3.6.1.4.1.311.2.2.10` encoded as OID *content*.
pub const OID_NTLMSSP: &[u8] = &[0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a];
// --- NegState --------------------------------------------------------------
/// Values of the `negState` field in NegTokenResp.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum NegState {
AcceptCompleted = 0,
AcceptIncomplete = 1,
Reject = 2,
RequestMic = 3,
}
impl NegState {
fn from_byte(b: u8) -> ProtoResult<Self> {
match b {
0 => Ok(NegState::AcceptCompleted),
1 => Ok(NegState::AcceptIncomplete),
2 => Ok(NegState::Reject),
3 => Ok(NegState::RequestMic),
_ => Err(ProtoError::Auth("invalid NegState")),
}
}
}
// --- DER length helpers ----------------------------------------------------
/// Encode a DER length (definite-length form, MS-SPNG always uses definite).
fn der_len(n: usize, out: &mut Vec<u8>) {
if n < 0x80 {
out.push(n as u8);
return;
}
// Long form. Find minimum number of bytes.
let mut tmp = [0u8; 8];
let mut nb = 0;
let mut v = n;
while v > 0 {
tmp[nb] = (v & 0xff) as u8;
v >>= 8;
nb += 1;
}
out.push(0x80 | nb as u8);
for i in (0..nb).rev() {
out.push(tmp[i]);
}
}
/// Read a DER length from `buf` starting at `pos`. Returns `(length, next_pos)`.
fn read_len(buf: &[u8], pos: usize) -> ProtoResult<(usize, usize)> {
if pos >= buf.len() {
return Err(ProtoError::Auth("DER length truncated"));
}
let first = buf[pos];
if first < 0x80 {
return Ok((first as usize, pos + 1));
}
let nb = (first & 0x7f) as usize;
if nb == 0 || nb > 4 {
// Indefinite (nb=0) — never used by SPNEGO.
// We cap at 4 bytes (max ~4 GiB), more than enough for tokens.
return Err(ProtoError::Auth("DER length form unsupported"));
}
if pos + 1 + nb > buf.len() {
return Err(ProtoError::Auth("DER length truncated"));
}
let mut v = 0usize;
for i in 0..nb {
v = (v << 8) | buf[pos + 1 + i] as usize;
}
Ok((v, pos + 1 + nb))
}
/// Read `(tag, content_slice, next_pos)` at `pos`. Verifies the expected tag.
fn read_tlv(buf: &[u8], pos: usize, expected_tag: u8) -> ProtoResult<(&[u8], usize)> {
if pos >= buf.len() {
return Err(ProtoError::Auth("DER tag truncated"));
}
if buf[pos] != expected_tag {
return Err(ProtoError::Auth("unexpected DER tag"));
}
let (len, after_len) = read_len(buf, pos + 1)?;
let end = after_len + len;
if end > buf.len() {
return Err(ProtoError::Auth("DER content truncated"));
}
Ok((&buf[after_len..end], end))
}
/// Read any TLV (returning its tag plus the content slice & end position).
fn read_any_tlv(buf: &[u8], pos: usize) -> ProtoResult<(u8, &[u8], usize)> {
if pos >= buf.len() {
return Err(ProtoError::Auth("DER tag truncated"));
}
let tag = buf[pos];
let (len, after_len) = read_len(buf, pos + 1)?;
let end = after_len + len;
if end > buf.len() {
return Err(ProtoError::Auth("DER content truncated"));
}
Ok((tag, &buf[after_len..end], end))
}
// --- TLV writer helper -----------------------------------------------------
fn write_tlv(tag: u8, content: &[u8], out: &mut Vec<u8>) {
out.push(tag);
der_len(content.len(), out);
out.extend_from_slice(content);
}
// --- Public API ------------------------------------------------------------
/// Decoded `NegTokenInit` payload — only the bits we care about.
#[derive(Debug, Clone)]
pub struct NegTokenInit {
/// List of mechanism OIDs (each entry is the OID content bytes, no 0x06 tag).
pub mech_types: Vec<Vec<u8>>,
/// `mechToken [2]` if present — typically the NTLMSSP NEGOTIATE_MESSAGE bytes.
pub mech_token: Option<Vec<u8>>,
}
/// Decoded `NegTokenResp` payload.
#[derive(Debug, Clone, Default)]
pub struct NegTokenResp {
pub neg_state: Option<NegState>,
/// `supportedMech [1]` (OID content bytes).
pub supported_mech: Option<Vec<u8>>,
/// `responseToken [2]` — typically inner NTLMSSP CHALLENGE/AUTHENTICATE bytes.
pub response_token: Option<Vec<u8>>,
pub mech_list_mic: Option<Vec<u8>>,
}
/// Decode the **initial** SPNEGO blob from the client. This is wrapped in
/// the GSS-API outer `[APPLICATION 0]` tag, contains a `thisMech` OID
/// (SPNEGO), and a `[0] NegTokenInit`.
///
/// Returns the parsed `NegTokenInit`.
pub fn decode_init_token(buf: &[u8]) -> ProtoResult<NegTokenInit> {
// [APPLICATION 0] IMPLICIT SEQUENCE { thisMech OID, NegotiationToken }
let (gss_inner, _end) = read_tlv(buf, 0, TAG_APP_0)?;
// thisMech
let (mech, after_mech) = read_tlv(gss_inner, 0, TAG_OBJECT)?;
if mech != OID_SPNEGO {
return Err(ProtoError::Auth("not an SPNEGO token"));
}
// NegotiationToken — choice tagged [0] for init.
let (init_inner, _) = read_tlv(gss_inner, after_mech, TAG_CTX_0)?;
parse_neg_token_init_body(init_inner)
}
fn parse_neg_token_init_body(inner: &[u8]) -> ProtoResult<NegTokenInit> {
// Inner is a SEQUENCE.
let (seq_body, _) = read_tlv(inner, 0, TAG_SEQUENCE)?;
let mut pos = 0usize;
let mut mech_types: Vec<Vec<u8>> = Vec::new();
let mut mech_token: Option<Vec<u8>> = None;
while pos < seq_body.len() {
let (tag, content, next) = read_any_tlv(seq_body, pos)?;
match tag {
TAG_CTX_0 => {
// mechTypes [0] MechTypeList ::= SEQUENCE OF MechType (OID)
let (mt_seq, _) = read_tlv(content, 0, TAG_SEQUENCE)?;
let mut p = 0usize;
while p < mt_seq.len() {
let (oid, e) = read_tlv(mt_seq, p, TAG_OBJECT)?;
mech_types.push(oid.to_vec());
p = e;
}
}
TAG_CTX_1 => {
// reqFlags — ignored.
}
TAG_CTX_2 => {
// mechToken [2] OCTET STRING
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
mech_token = Some(oct.to_vec());
}
TAG_CTX_3 => {
// mechListMIC — ignored on init.
}
_ => {
// Unknown — skip silently (forward-compat).
}
}
pos = next;
}
Ok(NegTokenInit {
mech_types,
mech_token,
})
}
/// Decode a subsequent `NegTokenResp`. These are sent without the GSS-API
/// outer wrapper — they begin directly with the `[1]` choice tag.
pub fn decode_resp_token(buf: &[u8]) -> ProtoResult<NegTokenResp> {
let (resp_inner, _) = read_tlv(buf, 0, TAG_CTX_1)?;
let (seq_body, _) = read_tlv(resp_inner, 0, TAG_SEQUENCE)?;
let mut pos = 0usize;
let mut out = NegTokenResp::default();
while pos < seq_body.len() {
let (tag, content, next) = read_any_tlv(seq_body, pos)?;
match tag {
TAG_CTX_0 => {
let (en, _) = read_tlv(content, 0, TAG_ENUMERATED)?;
if en.len() != 1 {
return Err(ProtoError::Auth("NegState ENUMERATED not 1 byte"));
}
out.neg_state = Some(NegState::from_byte(en[0])?);
}
TAG_CTX_1 => {
let (oid, _) = read_tlv(content, 0, TAG_OBJECT)?;
out.supported_mech = Some(oid.to_vec());
}
TAG_CTX_2 => {
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
out.response_token = Some(oct.to_vec());
}
TAG_CTX_3 => {
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
out.mech_list_mic = Some(oct.to_vec());
}
_ => {}
}
pos = next;
}
Ok(out)
}
/// Encode the **initial** server response to NEGOTIATE — a GSS-API-wrapped
/// `NegTokenInit` advertising NTLMSSP only. Used during SMB2 NEGOTIATE
/// when the server publishes its security blob.
pub fn encode_init_response() -> Vec<u8> {
// mechTypes SEQUENCE { OID NTLMSSP }
let mut mech_types_seq = Vec::new();
write_tlv(TAG_OBJECT, OID_NTLMSSP, &mut mech_types_seq);
let mut mech_types_outer = Vec::new();
write_tlv(TAG_SEQUENCE, &mech_types_seq, &mut mech_types_outer);
// mechTypes is [0] tagged.
let mut mech_types_ctx0 = Vec::new();
write_tlv(TAG_CTX_0, &mech_types_outer, &mut mech_types_ctx0);
// NegTokenInit SEQUENCE { mechTypes [0] }
let mut neg_token_init = Vec::new();
write_tlv(TAG_SEQUENCE, &mech_types_ctx0, &mut neg_token_init);
// [0] NegTokenInit (negotiationToken choice)
let mut choice_init = Vec::new();
write_tlv(TAG_CTX_0, &neg_token_init, &mut choice_init);
// Inside [APPLICATION 0]: { OID SPNEGO, [0] NegTokenInit }
let mut gss_inner = Vec::new();
write_tlv(TAG_OBJECT, OID_SPNEGO, &mut gss_inner);
gss_inner.extend_from_slice(&choice_init);
let mut out = Vec::new();
write_tlv(TAG_APP_0, &gss_inner, &mut out);
out
}
/// Encode a `NegTokenResp` wrapping the server's response token (typically
/// the NTLMSSP CHALLENGE_MESSAGE or a final empty-token AcceptCompleted).
///
/// `supported_mech` is included only with `AcceptIncomplete` (i.e. the very
/// first response to a NegTokenInit) — per RFC 4178 §4.2.2.
pub fn encode_resp_token(
state: NegState,
supported_mech: Option<&[u8]>,
response_token: Option<&[u8]>,
mech_list_mic: Option<&[u8]>,
) -> Vec<u8> {
let mut seq = Vec::new();
// [0] negState
{
let mut en = Vec::new();
write_tlv(TAG_ENUMERATED, &[state as u8], &mut en);
write_tlv(TAG_CTX_0, &en, &mut seq);
}
// [1] supportedMech
if let Some(oid) = supported_mech {
let mut o = Vec::new();
write_tlv(TAG_OBJECT, oid, &mut o);
write_tlv(TAG_CTX_1, &o, &mut seq);
}
// [2] responseToken
if let Some(tok) = response_token {
let mut o = Vec::new();
write_tlv(TAG_OCTET, tok, &mut o);
write_tlv(TAG_CTX_2, &o, &mut seq);
}
// [3] mechListMIC
if let Some(mic) = mech_list_mic {
let mut o = Vec::new();
write_tlv(TAG_OCTET, mic, &mut o);
write_tlv(TAG_CTX_3, &o, &mut seq);
}
let mut inner = Vec::new();
write_tlv(TAG_SEQUENCE, &seq, &mut inner);
let mut out = Vec::new();
write_tlv(TAG_CTX_1, &inner, &mut out);
out
}
// ===========================================================================
// Tests
// ===========================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn der_len_short() {
let mut v = Vec::new();
der_len(0x42, &mut v);
assert_eq!(v, [0x42]);
}
#[test]
fn der_len_long_one_byte() {
let mut v = Vec::new();
der_len(0xC8, &mut v);
assert_eq!(v, [0x81, 0xC8]);
}
#[test]
fn der_len_long_two_byte() {
let mut v = Vec::new();
der_len(0x1234, &mut v);
assert_eq!(v, [0x82, 0x12, 0x34]);
}
#[test]
fn read_len_round_trip() {
for n in [0usize, 1, 0x7F, 0x80, 0xFF, 0x100, 0xFFFF, 0x10000] {
let mut buf = Vec::new();
der_len(n, &mut buf);
let (got, next) = read_len(&buf, 0).unwrap();
assert_eq!(got, n);
assert_eq!(next, buf.len());
}
}
#[test]
fn init_response_is_decodable() {
let blob = encode_init_response();
// Must start with [APPLICATION 0] (0x60) tag.
assert_eq!(blob[0], TAG_APP_0);
// Decode with our own decoder going via decode_init_token.
// We craft a synthetic "init" by appending an empty mechToken? — not
// needed; decode_init_token tolerates absence. Test that the OID and
// the [0] mechTypes are reachable.
let init = decode_init_token(&blob).unwrap();
assert_eq!(init.mech_types.len(), 1);
assert_eq!(init.mech_types[0], OID_NTLMSSP);
assert!(init.mech_token.is_none());
}
#[test]
fn resp_token_round_trip_with_response() {
let payload = b"\x01\x02\x03\x04inner-blob";
let enc = encode_resp_token(
NegState::AcceptIncomplete,
Some(OID_NTLMSSP),
Some(payload),
None,
);
let dec = decode_resp_token(&enc).unwrap();
assert_eq!(dec.neg_state, Some(NegState::AcceptIncomplete));
assert_eq!(dec.supported_mech.as_deref(), Some(OID_NTLMSSP));
assert_eq!(dec.response_token.as_deref(), Some(&payload[..]));
assert!(dec.mech_list_mic.is_none());
}
#[test]
fn resp_token_round_trip_completed() {
let enc = encode_resp_token(NegState::AcceptCompleted, None, None, None);
let dec = decode_resp_token(&enc).unwrap();
assert_eq!(dec.neg_state, Some(NegState::AcceptCompleted));
assert!(dec.supported_mech.is_none());
assert!(dec.response_token.is_none());
}
#[test]
fn resp_token_with_mic() {
let mic = vec![0xAAu8; 16];
let enc = encode_resp_token(NegState::AcceptCompleted, None, None, Some(&mic));
let dec = decode_resp_token(&enc).unwrap();
assert_eq!(dec.mech_list_mic.as_deref(), Some(mic.as_slice()));
}
/// Build a NegTokenInit by hand (containing a mechToken) and decode it.
#[test]
fn decode_init_with_mech_token() {
let inner_token = b"NTLMSSP\x00fakeNegotiate";
// mechTypes
let mut mts = Vec::new();
write_tlv(TAG_OBJECT, OID_NTLMSSP, &mut mts);
let mut mts_seq = Vec::new();
write_tlv(TAG_SEQUENCE, &mts, &mut mts_seq);
let mut mts_ctx0 = Vec::new();
write_tlv(TAG_CTX_0, &mts_seq, &mut mts_ctx0);
// mechToken [2] OCTET STRING
let mut mt_oct = Vec::new();
write_tlv(TAG_OCTET, inner_token, &mut mt_oct);
let mut mt_ctx2 = Vec::new();
write_tlv(TAG_CTX_2, &mt_oct, &mut mt_ctx2);
// SEQUENCE { [0] mechTypes, [2] mechToken }
let mut seq = Vec::new();
seq.extend_from_slice(&mts_ctx0);
seq.extend_from_slice(&mt_ctx2);
let mut neg_token_init = Vec::new();
write_tlv(TAG_SEQUENCE, &seq, &mut neg_token_init);
let mut choice = Vec::new();
write_tlv(TAG_CTX_0, &neg_token_init, &mut choice);
let mut gss_inner = Vec::new();
write_tlv(TAG_OBJECT, OID_SPNEGO, &mut gss_inner);
gss_inner.extend_from_slice(&choice);
let mut blob = Vec::new();
write_tlv(TAG_APP_0, &gss_inner, &mut blob);
let dec = decode_init_token(&blob).unwrap();
assert_eq!(dec.mech_types.len(), 1);
assert_eq!(dec.mech_types[0], OID_NTLMSSP);
assert_eq!(dec.mech_token.as_deref(), Some(&inner_token[..]));
}
#[test]
fn rejects_non_spnego_oid() {
// Build a GSS token with a different OID inside.
let bad_oid = [0x2bu8, 0x06, 0x01, 0x01, 0x01, 0x01];
let mut gss_inner = Vec::new();
write_tlv(TAG_OBJECT, &bad_oid, &mut gss_inner);
// Empty [0] payload.
let mut empty = Vec::new();
write_tlv(TAG_SEQUENCE, &[], &mut empty);
let mut choice = Vec::new();
write_tlv(TAG_CTX_0, &empty, &mut choice);
gss_inner.extend_from_slice(&choice);
let mut blob = Vec::new();
write_tlv(TAG_APP_0, &gss_inner, &mut blob);
let err = decode_init_token(&blob).unwrap_err();
assert!(matches!(err, ProtoError::Auth(_)));
}
#[test]
fn rejects_truncated_blob() {
let err = decode_init_token(&[0x60, 0x05, 0xAA, 0xBB]).unwrap_err();
assert!(matches!(err, ProtoError::Auth(_)));
}
}