Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Advanced Features

Mandatory integrity labels

Windows integrity levels control write-up/read-down/execute-up policies. Each level maps to a SID under authority 16:

#![allow(unused)]
fn main() {
use win_sd::{Ace, IntegrityLevel, IntegrityPolicy};

assert!(IntegrityLevel::Low < IntegrityLevel::Medium);
assert!(IntegrityLevel::Medium < IntegrityLevel::High);

let sid = IntegrityLevel::Medium.to_sid();
assert_eq!(sid.to_string(), "S-1-16-8192");

let ace = Ace::mandatory_label(
    IntegrityLevel::High,
    IntegrityPolicy::NO_WRITE_UP | IntegrityPolicy::NO_READ_UP,
);
assert_eq!(ace.integrity_level(), Some(IntegrityLevel::High));
}

Conditional expressions

Callback ACEs (types XA, XD, ZA, ZD, XU, ZU) carry conditional expressions in their application_data field, encoded in the MS-DTYP binary postfix (RPN) format per §2.4.4.17. The crate provides two interfaces: a text-based SDDL syntax and the underlying binary format.

SDDL text syntax

The sddl_conditional module parses and generates the human-readable SDDL conditional expression syntax:

#![allow(unused)]
fn main() {
use win_sd::sddl_conditional::{parse_sddl_condition, generate_sddl_condition};

// Parse from text
let expr = parse_sddl_condition(r#"@User.dept == "Sales""#).unwrap();

// Generate text (round-trips with correct precedence)
let text = generate_sddl_condition(&expr);
assert_eq!(text, r#"@User.dept == "Sales""#);

// Complex expressions with logical operators
let expr = parse_sddl_condition(
    r#"@User.dept == "Sales" && @Resource.public == 1"#
).unwrap();

// Membership operators
let expr = parse_sddl_condition("Member_of{SID(BA)}").unwrap();

// Composite literals
let expr = parse_sddl_condition(
    r#"@User.groups Contains {"admin", "devs"}"#
).unwrap();

// Existence checks
let expr = parse_sddl_condition("Exists @User.clearance").unwrap();
}

Supported SDDL syntax:

ElementExamples
Attribute references@User.name, @Resource.name, @Device.name, @Local.name
Comparison==, !=, <, <=, >, >=
Set operatorsContains, Any_of
Logical&&, ||, !(...)
ExistenceExists @User.attr
MembershipMember_of{SID(BA)}, Not_Member_of{...}, Member_of_Any{...}, Device_Member_of{...}, plus Not_Device_Member_of, Device_Member_of_Any, Not_Member_of_Any, Not_Device_Member_of_Any
Literalsintegers, "strings", SID(BA), SID(S-1-5-...), {"a", "b"}

The parser enforces a depth limit of 128 to prevent stack overflow from deeply nested input. Trailing unparsed input is rejected.

AST and value types

The ConditionalExpr enum has 22 variants:

CategoryVariants
Leaf nodesLiteral(ClaimValue), UserAttr, ResourceAttr, DeviceAttr, LocalAttr
ComparisonEquals, NotEquals, LessThan, LessEqual, GreaterThan, GreaterEqual
SetContains, AnyOf
LogicalAnd, Or, Not
ExistenceExists
MembershipMemberOf, DeviceMemberOf, NotMemberOf, NotDeviceMemberOf, MemberOfAny, DeviceMemberOfAny, NotMemberOfAny, NotDeviceMemberOfAny

The ClaimValue enum carries typed data:

VariantRust typeDescription
Int64i6464-bit signed integer
Uint64u6464-bit unsigned integer
StringStringUnicode string
SidSidSecurity identifier
BooleanboolBoolean value
OctetStringVec<u8>Raw byte string
CompositeVec<ClaimValue>Multi-valued set

Building expressions programmatically

#![allow(unused)]
fn main() {
use win_sd::conditional::*;

let expr = ConditionalExpr::Equals(
    Box::new(ConditionalExpr::UserAttr("department".to_string())),
    Box::new(ConditionalExpr::Literal(ClaimValue::String("Engineering".to_string()))),
);
}

Binary format

The binary format uses the artx signature prefix (0x61 0x72 0x74 0x78):

#![allow(unused)]
fn main() {
use win_sd::conditional::*;

let expr = ConditionalExpr::Equals(
    Box::new(ConditionalExpr::UserAttr("department".to_string())),
    Box::new(ConditionalExpr::Literal(ClaimValue::String("Engineering".to_string()))),
);

// Write to binary
let binary = write_conditional(&expr);
assert!(is_conditional_ace_data(&binary));

// Parse back
let parsed = parse_conditional(&binary).unwrap();
assert_eq!(expr, parsed);
}

Extracting conditions from callback ACEs

#![allow(unused)]
fn main() {
use win_sd::Ace;

fn inspect_ace(ace: &Ace) {
    if ace.ace_type().is_callback() {
        if let Some(result) = ace.condition() {
            match result {
                Ok(expr) => println!("condition: {:?}", expr),
                Err(e) => println!("parse error: {e}"),
            }
        }
    }
}
}

Evaluation

Evaluation uses three-valued logic via ConditionalResult:

  • True — the expression matched; the ACE applies.
  • False — the expression did not match; the ACE is skipped.
  • Unknown — an attribute was missing or a type mismatch occurred. For allow ACEs, Unknown means “not granted.” For deny ACEs, Unknown means “denied” (fail-secure).
#![allow(unused)]
fn main() {
use win_sd::conditional::*;

struct MyCtx;
impl AttributeContext for MyCtx {
    fn get_user_attr(&self, name: &str) -> Option<ClaimValue> {
        if name == "department" {
            Some(ClaimValue::String("Engineering".to_string()))
        } else { None }
    }
    fn get_resource_attr(&self, _: &str) -> Option<ClaimValue> { None }
    fn get_device_attr(&self, _: &str) -> Option<ClaimValue> { None }
    fn get_local_attr(&self, _: &str) -> Option<ClaimValue> { None }
    fn is_member_of(&self, _: &win_sd::Sid) -> bool { false }
    fn is_device_member_of(&self, _: &win_sd::Sid) -> bool { false }
}

let expr = ConditionalExpr::Equals(
    Box::new(ConditionalExpr::UserAttr("department".to_string())),
    Box::new(ConditionalExpr::Literal(ClaimValue::String("Engineering".to_string()))),
);
assert_eq!(expr.evaluate(&MyCtx), ConditionalResult::True);

// Missing attribute returns Unknown
let missing = ConditionalExpr::Exists(
    Box::new(ConditionalExpr::UserAttr("clearance".to_string())),
);
assert_eq!(missing.evaluate(&MyCtx), ConditionalResult::False);
}

The AttributeContext trait has six methods:

MethodPurpose
get_user_attr(name)Look up a user claim attribute
get_resource_attr(name)Look up a resource attribute
get_device_attr(name)Look up a device claim attribute
get_local_attr(name)Look up a local attribute
is_member_of(sid)Check token SID membership
is_device_member_of(sid)Check device SID membership

EmptyAttributeContext is a provided no-op implementation returning None/false for all lookups.

The evaluator has a depth limit of 1024 to prevent stack overflow from crafted inputs.

Error handling

parse_conditional returns ConditionalParseError with these variants:

VariantMeaning
InvalidSignatureData doesn’t start with artx
UnexpectedEndTruncated data
UnknownToken(u8)Unrecognized token byte
StackUnderflowMalformed RPN: not enough operands

Binary serialization

Security descriptors can be serialized to/from the MS-DTYP self-relative binary format:

#![allow(unused)]
fn main() {
use win_sd::{SecurityDescriptorBuilder, SecurityDescriptor, AccessMask, Sid};

let sd = SecurityDescriptorBuilder::new()
    .owner(Sid::local_system())
    .allow(Sid::everyone(), AccessMask::FILE_GENERIC_READ)
    .build();

// Serialize to binary
let bytes = sd.to_bytes();

// Parse back
let recovered = SecurityDescriptor::from_bytes(&bytes).unwrap();
assert_eq!(recovered.owner(), sd.owner());
}

The parser validates SID sub-authority counts, minimum ACE sizes, AclSize bounds, and rejects overlapping component offset regions.

ACL inheritance

The inheritance engine propagates ACEs from parent to child objects:

#![allow(unused)]
fn main() {
use win_sd::{SecurityDescriptorBuilder, AccessMask, Sid, AceFlags};
use win_sd::inheritance::create_child_sd;

let parent = SecurityDescriptorBuilder::new()
    .owner(Sid::local_system())
    .allow_with_flags(
        Sid::administrators(),
        AccessMask::FILE_ALL_ACCESS,
        AceFlags::CONTAINER_INHERIT | AceFlags::OBJECT_INHERIT,
    )
    .build();

// Create a child directory SD
let child = create_child_sd(&parent, true);
assert!(child.dacl().is_some());

// Inherited ACEs have the INHERITED_ACE flag set
let ace = &child.dacl().unwrap().entries()[0];
assert!(ace.is_inherited());

// INHERIT_ONLY is cleared: the ACE applies to the child
assert!(!ace.is_inherit_only());
}

Inheritance rules:

  • Containers: ACEs with CONTAINER_INHERIT are inherited. ACEs with only OBJECT_INHERIT pass through as INHERIT_ONLY.
  • Non-containers: ACEs with OBJECT_INHERIT are inherited, with all inheritance flags cleared.
  • NO_PROPAGATE_INHERIT stops further propagation.
  • INHERIT_ONLY on the parent means “skip the parent, apply to children” — it is cleared on the inherited copy.

Access tokens

The AccessToken consolidates all access-check inputs:

#![allow(unused)]
fn main() {
use win_sd::Sid;
use win_sd::token::{AccessToken, Privilege};
use win_sd::IntegrityLevel;

let token = AccessToken::new(Sid::from_authority(5, vec![21, 100, 200, 300, 1001]))
    .with_groups([Sid::administrators(), Sid::everyone()])
    .with_integrity(IntegrityLevel::High)
    .with_privilege(Privilege::SeSecurityPrivilege);

assert_eq!(token.integrity_level(), IntegrityLevel::High);
assert!(token.has_privilege(Privilege::SeSecurityPrivilege));

// Restricted tokens limit the second DACL walk
let restricted = token.with_restricted_sids(vec![Sid::authenticated_users()]);
assert!(restricted.is_restricted());
}

Resource attribute claims

ClaimSecurityAttribute models the CLAIM_SECURITY_ATTRIBUTE_RELATIVE_V1 structure used in SYSTEM_RESOURCE_ATTRIBUTE_ACE:

#![allow(unused)]
fn main() {
use win_sd::claims::{ClaimSecurityAttribute, ClaimValueType};
use win_sd::conditional::ClaimValue;

let attr = ClaimSecurityAttribute::new(
    "Department",
    ClaimValueType::String,
    vec![ClaimValue::String("Engineering".to_string())],
);
assert_eq!(attr.name(), "Department");
}