From 8d47f137f97ebba481f21f7255579fa5bf98174e Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Wed, 1 Apr 2026 15:15:03 -0700 Subject: [PATCH 1/5] Static SDDL processing --- include/wil/token_helpers.h | 271 ++++++++++++++++++++++++++++++++++++ tests/TokenHelpersTests.cpp | 118 ++++++++++++++++ 2 files changed, 389 insertions(+) diff --git a/include/wil/token_helpers.h b/include/wil/token_helpers.h index bce91206a..ada3b9d03 100644 --- a/include/wil/token_helpers.h +++ b/include/wil/token_helpers.h @@ -555,6 +555,11 @@ namespace details SID_IDENTIFIER_AUTHORITY IdentifierAuthority; DWORD SubAuthority[AuthorityCount]; + static constexpr size_t byte_size() + { + return 8 + 4 * AuthorityCount; + } + PSID get() { return reinterpret_cast(this); @@ -608,6 +613,272 @@ constexpr auto make_static_nt_sid(Ts&&... subAuthorities) return make_static_sid(SECURITY_NT_AUTHORITY, wistd::forward(subAuthorities)...); } +#ifdef _HAS_CXX20 + +/// @cond +namespace details +{ + // Byte-level constexpr writers for building self-relative security descriptors. + constexpr void write_byte(BYTE* dest, size_t& offset, BYTE value) + { + dest[offset++] = value; + } + + constexpr void write_word(BYTE* dest, size_t& offset, WORD value) + { + dest[offset++] = static_cast(value & 0xFF); + dest[offset++] = static_cast((value >> 8) & 0xFF); + } + + constexpr void write_dword(BYTE* dest, size_t& offset, DWORD value) + { + dest[offset++] = static_cast(value & 0xFF); + dest[offset++] = static_cast((value >> 8) & 0xFF); + dest[offset++] = static_cast((value >> 16) & 0xFF); + dest[offset++] = static_cast((value >> 24) & 0xFF); + } + + template + constexpr void write_sid(BYTE* dest, size_t& offset, const static_sid_t& sid) + { + write_byte(dest, offset, sid.Revision); + write_byte(dest, offset, sid.SubAuthorityCount); + for (int i = 0; i < 6; ++i) + { + write_byte(dest, offset, sid.IdentifierAuthority.Value[i]); + } + for (size_t i = 0; i < N; ++i) + { + write_dword(dest, offset, sid.SubAuthority[i]); + } + } + + template + constexpr size_t sid_byte_size(const static_sid_t&) + { + return 8 + 4 * N; + } + + // A typed ACE containing the ACE header fields and an embedded SID. + // Layout mirrors ACCESS_ALLOWED_ACE / ACCESS_DENIED_ACE (same binary layout). + template + struct static_ace_t + { + BYTE AceType; + BYTE AceFlags; + ACCESS_MASK Mask; + static_sid_t Sid; + + static constexpr size_t byte_size() + { + // ACE_HEADER (Type:1 + Flags:1 + Size:2) + Mask (4) + SID (8 + 4*N) + return 4 + 4 + 8 + 4 * SubAuthorityCount; + } + }; + + template + constexpr void write_ace(BYTE* dest, size_t& offset, const static_ace_t& ace) + { + auto aceSize = static_cast(static_ace_t::byte_size()); + // ACE_HEADER + write_byte(dest, offset, ace.AceType); + write_byte(dest, offset, ace.AceFlags); + write_word(dest, offset, aceSize); + // Mask + write_dword(dest, offset, ace.Mask); + // SID (replaces the SidStart DWORD in the Win32 ACE struct) + write_sid(dest, offset, ace.Sid); + } + + // Compute the total byte size of a list of ACEs from their types. + template + constexpr size_t total_ace_size() + { + return (0 + ... + Aces::byte_size()); + } + + // Sentinel type used when no group SID is desired. + struct no_sid_t + { + static constexpr size_t byte_size() + { + return 0; + } + }; + + constexpr size_t sid_byte_size(const no_sid_t&) + { + return 0; + } + + constexpr void write_sid(BYTE*, size_t&, const no_sid_t&) + { + } + + // The result type: a fixed-size byte array that is layout-compatible with SECURITY_DESCRIPTOR_RELATIVE. + template + struct static_security_descriptor_t + { + alignas(DWORD) BYTE data[TotalSize]{}; + + PSECURITY_DESCRIPTOR get() + { + return reinterpret_cast(data); + } + + PSECURITY_DESCRIPTOR get() const + { + return reinterpret_cast(const_cast(data)); + } + }; + + // Runtime/constexpr check: deny ACEs must precede allow ACEs in the variadic pack. + constexpr bool deny_before_allow_check(const BYTE* types, size_t count) + { + bool seenAllow = false; + for (size_t i = 0; i < count; ++i) + { + if (types[i] == ACCESS_ALLOWED_ACE_TYPE) + { + seenAllow = true; + } + else if (types[i] == ACCESS_DENIED_ACE_TYPE && seenAllow) + { + return false; // deny after allow + } + } + return true; + } +} // namespace details +/// @endcond + +//! Sentinel value for omitting the group SID in make_self_relative_sd. +inline constexpr details::no_sid_t no_sid{}; + +/** Constructs a constexpr ACCESS_ALLOWED ACE with the given access mask and SID. +@code +auto ace = wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID)); +@endcode +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +@return A static_ace_t suitable for passing to make_self_relative_sd. +*/ +template +constexpr auto make_allow_ace(ACCESS_MASK mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, 0, mask, sid}; +} + +/** Constructs a constexpr ACCESS_ALLOWED ACE with inheritance flags. +@param flags ACE inheritance flags (e.g. CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE). +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +*/ +template +constexpr auto make_allow_ace(BYTE flags, ACCESS_MASK mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, flags, mask, sid}; +} + +/** Constructs a constexpr ACCESS_DENIED ACE with the given access mask and SID. +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +*/ +template +constexpr auto make_deny_ace(ACCESS_MASK mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, 0, mask, sid}; +} + +/** Constructs a constexpr ACCESS_DENIED ACE with inheritance flags. +@param flags ACE inheritance flags (e.g. CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE). +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +*/ +template +constexpr auto make_deny_ace(BYTE flags, ACCESS_MASK mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, flags, mask, sid}; +} + +/** Constructs a constexpr self-relative SECURITY_DESCRIPTOR from an owner SID, group SID, and a set of ACEs. +The resulting structure is a contiguous byte array laid out as a valid self-relative security descriptor +that can be passed directly to any Win32 API accepting PSECURITY_DESCRIPTOR. + +Deny ACEs must precede allow ACEs in the parameter list (enforced at compile time). +Pass wil::no_sid to omit the group SID. + +@code +constexpr auto sd = wil::make_self_relative_sd( + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), // owner + wil::no_sid, // no group + wil::make_deny_ace(GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_WORLD_RID)), // deny guests write + wil::make_allow_ace(GENERIC_READ | GENERIC_WRITE, + wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); // allow authenticated users +@endcode + +@param owner The owner SID (a static_sid_t or no_sid). +@param group The group SID (a static_sid_t or no_sid). +@param aces One or more ACEs built with make_allow_ace or make_deny_ace. +@return A static_security_descriptor_t containing a valid self-relative SECURITY_DESCRIPTOR. +*/ +template +constexpr auto make_self_relative_sd(const OwnerSid& owner, const GroupSid& group, const Aces&... aces) +{ + static_assert(sizeof...(aces) > 0, "at least one ACE is required"); + + constexpr size_t sdHeaderSize = 20; // SECURITY_DESCRIPTOR_RELATIVE header + constexpr size_t aclHeaderSize = 8; // ACL header + constexpr size_t ownerSize = OwnerSid::byte_size(); + constexpr size_t groupSize = GroupSid::byte_size(); + constexpr size_t acesSize = details::total_ace_size(); + constexpr size_t totalSize = sdHeaderSize + ownerSize + groupSize + aclHeaderSize + acesSize; + + // Deny ACEs must precede allow ACEs — will fail constexpr evaluation if violated. + BYTE aceTypes[] = {aces.AceType...}; + WI_ASSERT(details::deny_before_allow_check(aceTypes, sizeof...(aces))); + + details::static_security_descriptor_t result{}; + size_t offset = 0; + + // SECURITY_DESCRIPTOR_RELATIVE header + constexpr WORD control = SE_SELF_RELATIVE | SE_DACL_PRESENT; + constexpr DWORD offsetOwner = (ownerSize > 0) ? static_cast(sdHeaderSize) : 0; + constexpr DWORD offsetGroup = (groupSize > 0) ? static_cast(sdHeaderSize + ownerSize) : 0; + constexpr DWORD offsetSacl = 0; // no SACL + constexpr DWORD offsetDacl = static_cast(sdHeaderSize + ownerSize + groupSize); + + details::write_byte(result.data, offset, SECURITY_DESCRIPTOR_REVISION); // Revision + details::write_byte(result.data, offset, 0); // Sbz1 + details::write_word(result.data, offset, control); + details::write_dword(result.data, offset, offsetOwner); + details::write_dword(result.data, offset, offsetGroup); + details::write_dword(result.data, offset, offsetSacl); + details::write_dword(result.data, offset, offsetDacl); + + // Owner SID + details::write_sid(result.data, offset, owner); + + // Group SID + details::write_sid(result.data, offset, group); + + // DACL: ACL header + constexpr auto aclSize = static_cast(aclHeaderSize + acesSize); + constexpr auto aceCount = static_cast(sizeof...(aces)); + details::write_byte(result.data, offset, ACL_REVISION); // AclRevision + details::write_byte(result.data, offset, 0); // Sbz1 + details::write_word(result.data, offset, aclSize); // AclSize + details::write_word(result.data, offset, aceCount); // AceCount + details::write_word(result.data, offset, 0); // Sbz2 + + // ACEs + (details::write_ace(result.data, offset, aces), ...); + + return result; +} + +#endif // _HAS_CXX20 + /** Determines whether a specified security identifier (SID) is enabled in an access token. This function determines whether a security identifier, described by a given set of subauthorities, is enabled in the given access token. Note that only up to eight subauthorities can be passed to this function. diff --git a/tests/TokenHelpersTests.cpp b/tests/TokenHelpersTests.cpp index c99907b24..b51178971 100644 --- a/tests/TokenHelpersTests.cpp +++ b/tests/TokenHelpersTests.cpp @@ -280,6 +280,124 @@ TEST_CASE("TokenHelpersTests::StaticSid", "[token_helpers]") REQUIRE(*GetSidSubAuthority(staticSid.get(), 1) == DOMAIN_ALIAS_RID_GUESTS); } +#ifdef _HAS_CXX20 +TEST_CASE("TokenHelpersTests::SelfRelativeSD_BasicAllowAce", "[token_helpers]") +{ + auto ownerSid = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + auto sd = wil::make_self_relative_sd( + ownerSid, + wil::no_sid, + wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify owner + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(sd.get(), &pOwner, &ownerDefaulted)); + REQUIRE(pOwner != nullptr); + REQUIRE(EqualSid(pOwner, ownerSid.get())); + + // Verify no group + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(sd.get(), &pGroup, &groupDefaulted)); + REQUIRE(pGroup == nullptr); + + // Verify DACL present + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + REQUIRE(daclPresent); + REQUIRE(pDacl != nullptr); + + // Verify ACE + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + auto* ace = static_cast(pAce); + REQUIRE(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE); + REQUIRE(ace->Mask == GENERIC_READ); + auto aceSid = wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID); + REQUIRE(EqualSid(reinterpret_cast(&ace->SidStart), aceSid.get())); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_DenyAndAllowAces", "[token_helpers]") +{ + auto sd = wil::make_self_relative_sd( + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS), + wil::make_deny_ace(GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_GUESTS)), + wil::make_allow_ace(GENERIC_READ | GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify group SID is present + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(sd.get(), &pGroup, &groupDefaulted)); + REQUIRE(pGroup != nullptr); + auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); + REQUIRE(EqualSid(pGroup, expectedGroup.get())); + + // Verify DACL with 2 ACEs + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + REQUIRE(daclPresent); + REQUIRE(pDacl->AceCount == 2); + + // First ACE: deny + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_DENIED_ACE_TYPE); + REQUIRE(static_cast(pAce)->Mask == GENERIC_WRITE); + + // Second ACE: allow + REQUIRE(GetAce(pDacl, 1, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_ALLOWED_ACE_TYPE); + REQUIRE(static_cast(pAce)->Mask == (GENERIC_READ | GENERIC_WRITE)); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_InheritanceFlags", "[token_helpers]") +{ + auto sd = wil::make_self_relative_sd( + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), + wil::no_sid, + wil::make_allow_ace( + static_cast(CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE), + GENERIC_READ, + wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + auto* ace = static_cast(pAce); + REQUIRE((ace->Header.AceFlags & CONTAINER_INHERIT_ACE) != 0); + REQUIRE((ace->Header.AceFlags & OBJECT_INHERIT_ACE) != 0); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_Constexpr", "[token_helpers]") +{ + // Verify the SD can be constructed at compile time + constexpr auto sd = wil::make_self_relative_sd( + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), + wil::no_sid, + wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + // Validate at runtime that the constexpr result is a valid SD + auto mutableSd = sd; + REQUIRE(IsValidSecurityDescriptor(mutableSd.get())); +} +#endif // _HAS_CXX20 + #if (_WIN32_WINNT >= _WIN32_WINNT_WIN8) TEST_CASE("TokenHelpersTests::TestMembership", "[token_helpers]") { From 49cecaeabebf4e175a6bc053de545450e49e4dfc Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Wed, 1 Apr 2026 15:20:39 -0700 Subject: [PATCH 2/5] Add specific types to show owner vs group --- include/wil/token_helpers.h | 61 +++++++++++++++++++++++++++++++++---- tests/TokenHelpersTests.cpp | 32 +++++++++++++++++-- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/include/wil/token_helpers.h b/include/wil/token_helpers.h index ada3b9d03..1ab6260ee 100644 --- a/include/wil/token_helpers.h +++ b/include/wil/token_helpers.h @@ -732,6 +732,41 @@ namespace details } }; + // Tagged wrapper types for sd_owner() / sd_group() convenience helpers. + template + struct sd_owner_t + { + SidType sid; + + static constexpr size_t byte_size() + { + return SidType::byte_size(); + } + }; + + template + struct sd_group_t + { + SidType sid; + + static constexpr size_t byte_size() + { + return SidType::byte_size(); + } + }; + + template + constexpr void write_sid(BYTE* dest, size_t& offset, const sd_owner_t& owner) + { + write_sid(dest, offset, owner.sid); + } + + template + constexpr void write_sid(BYTE* dest, size_t& offset, const sd_group_t& group) + { + write_sid(dest, offset, group.sid); + } + // Runtime/constexpr check: deny ACEs must precede allow ACEs in the variadic pack. constexpr bool deny_before_allow_check(const BYTE* types, size_t count) { @@ -755,6 +790,20 @@ namespace details //! Sentinel value for omitting the group SID in make_self_relative_sd. inline constexpr details::no_sid_t no_sid{}; +//! Tags a SID as the owner for use with make_self_relative_sd. +template +constexpr auto sd_owner(const T& sid) +{ + return details::sd_owner_t{sid}; +} + +//! Tags a SID (or no_sid) as the group for use with make_self_relative_sd. +template +constexpr auto sd_group(const T& sid) +{ + return details::sd_group_t{sid}; +} + /** Constructs a constexpr ACCESS_ALLOWED ACE with the given access mask and SID. @code auto ace = wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID)); @@ -810,15 +859,15 @@ Pass wil::no_sid to omit the group SID. @code constexpr auto sd = wil::make_self_relative_sd( - wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), // owner - wil::no_sid, // no group - wil::make_deny_ace(GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_WORLD_RID)), // deny guests write + wil::sd_owner(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS)), + wil::sd_group(wil::no_sid), + wil::make_deny_ace(GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_WORLD_RID)), wil::make_allow_ace(GENERIC_READ | GENERIC_WRITE, - wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); // allow authenticated users + wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); @endcode -@param owner The owner SID (a static_sid_t or no_sid). -@param group The group SID (a static_sid_t or no_sid). +@param owner The owner SID (a static_sid_t, sd_owner_t, or no_sid). +@param group The group SID (a static_sid_t, sd_group_t, or no_sid). @param aces One or more ACEs built with make_allow_ace or make_deny_ace. @return A static_security_descriptor_t containing a valid self-relative SECURITY_DESCRIPTOR. */ diff --git a/tests/TokenHelpersTests.cpp b/tests/TokenHelpersTests.cpp index b51178971..9d4e88c5f 100644 --- a/tests/TokenHelpersTests.cpp +++ b/tests/TokenHelpersTests.cpp @@ -386,16 +386,42 @@ TEST_CASE("TokenHelpersTests::SelfRelativeSD_InheritanceFlags", "[token_helpers] TEST_CASE("TokenHelpersTests::SelfRelativeSD_Constexpr", "[token_helpers]") { - // Verify the SD can be constructed at compile time + // Verify the SD can be constructed at compile time using sd_owner/sd_group helpers constexpr auto sd = wil::make_self_relative_sd( - wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), - wil::no_sid, + wil::sd_owner(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS)), + wil::sd_group(wil::no_sid), wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); // Validate at runtime that the constexpr result is a valid SD auto mutableSd = sd; REQUIRE(IsValidSecurityDescriptor(mutableSd.get())); } + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_OwnerGroupHelpers", "[token_helpers]") +{ + // sd_owner + sd_group with real SIDs + auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS)), + wil::sd_group(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS)), + wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify owner + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(sd.get(), &pOwner, &ownerDefaulted)); + auto expectedOwner = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + REQUIRE(EqualSid(pOwner, expectedOwner.get())); + + // Verify group + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(sd.get(), &pGroup, &groupDefaulted)); + REQUIRE(pGroup != nullptr); + auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); + REQUIRE(EqualSid(pGroup, expectedGroup.get())); +} #endif // _HAS_CXX20 #if (_WIN32_WINNT >= _WIN32_WINNT_WIN8) From ca38c3bc5407feac46c2ff954dbbea9d78951aee Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Wed, 1 Apr 2026 15:28:34 -0700 Subject: [PATCH 3/5] Add constexpr self-relative SECURITY_DESCRIPTOR builder\n\nAdd make_self_relative_sd(), make_allow_ace(), make_deny_ace(),\nsd_owner(), sd_group(), and no_sid for composing self-relative\nsecurity descriptors at compile time from static SIDs and typed\nACEs. C++20 only, gated behind _HAS_CXX20.\n\nSupports allow/deny ACE types, per-ACE inheritance flags, and\noptional owner/group SIDs. Deny-before-allow ordering enforced\nvia WI_ASSERT." --- include/wil/token_helpers.h | 64 ++++++++++++++++++------------------- tests/TokenHelpersTests.cpp | 2 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/include/wil/token_helpers.h b/include/wil/token_helpers.h index 1ab6260ee..e861d8bbf 100644 --- a/include/wil/token_helpers.h +++ b/include/wil/token_helpers.h @@ -619,27 +619,27 @@ constexpr auto make_static_nt_sid(Ts&&... subAuthorities) namespace details { // Byte-level constexpr writers for building self-relative security descriptors. - constexpr void write_byte(BYTE* dest, size_t& offset, BYTE value) + constexpr void write_byte(uint8_t* dest, size_t& offset, uint8_t value) { dest[offset++] = value; } - constexpr void write_word(BYTE* dest, size_t& offset, WORD value) + constexpr void write_word(uint8_t* dest, size_t& offset, uint16_t value) { - dest[offset++] = static_cast(value & 0xFF); - dest[offset++] = static_cast((value >> 8) & 0xFF); + dest[offset++] = static_cast(value & 0xFF); + dest[offset++] = static_cast((value >> 8) & 0xFF); } - constexpr void write_dword(BYTE* dest, size_t& offset, DWORD value) + constexpr void write_dword(uint8_t* dest, size_t& offset, uint32_t value) { - dest[offset++] = static_cast(value & 0xFF); - dest[offset++] = static_cast((value >> 8) & 0xFF); - dest[offset++] = static_cast((value >> 16) & 0xFF); - dest[offset++] = static_cast((value >> 24) & 0xFF); + dest[offset++] = static_cast(value & 0xFF); + dest[offset++] = static_cast((value >> 8) & 0xFF); + dest[offset++] = static_cast((value >> 16) & 0xFF); + dest[offset++] = static_cast((value >> 24) & 0xFF); } template - constexpr void write_sid(BYTE* dest, size_t& offset, const static_sid_t& sid) + constexpr void write_sid(uint8_t* dest, size_t& offset, const static_sid_t& sid) { write_byte(dest, offset, sid.Revision); write_byte(dest, offset, sid.SubAuthorityCount); @@ -664,9 +664,9 @@ namespace details template struct static_ace_t { - BYTE AceType; - BYTE AceFlags; - ACCESS_MASK Mask; + uint8_t AceType; + uint8_t AceFlags; + uint32_t Mask; static_sid_t Sid; static constexpr size_t byte_size() @@ -677,9 +677,9 @@ namespace details }; template - constexpr void write_ace(BYTE* dest, size_t& offset, const static_ace_t& ace) + constexpr void write_ace(uint8_t* dest, size_t& offset, const static_ace_t& ace) { - auto aceSize = static_cast(static_ace_t::byte_size()); + auto aceSize = static_cast(static_ace_t::byte_size()); // ACE_HEADER write_byte(dest, offset, ace.AceType); write_byte(dest, offset, ace.AceFlags); @@ -711,7 +711,7 @@ namespace details return 0; } - constexpr void write_sid(BYTE*, size_t&, const no_sid_t&) + constexpr void write_sid(uint8_t*, size_t&, const no_sid_t&) { } @@ -719,7 +719,7 @@ namespace details template struct static_security_descriptor_t { - alignas(DWORD) BYTE data[TotalSize]{}; + alignas(uint32_t) uint8_t data[TotalSize]{}; PSECURITY_DESCRIPTOR get() { @@ -728,7 +728,7 @@ namespace details PSECURITY_DESCRIPTOR get() const { - return reinterpret_cast(const_cast(data)); + return reinterpret_cast(const_cast(data)); } }; @@ -756,19 +756,19 @@ namespace details }; template - constexpr void write_sid(BYTE* dest, size_t& offset, const sd_owner_t& owner) + constexpr void write_sid(uint8_t* dest, size_t& offset, const sd_owner_t& owner) { write_sid(dest, offset, owner.sid); } template - constexpr void write_sid(BYTE* dest, size_t& offset, const sd_group_t& group) + constexpr void write_sid(uint8_t* dest, size_t& offset, const sd_group_t& group) { write_sid(dest, offset, group.sid); } // Runtime/constexpr check: deny ACEs must precede allow ACEs in the variadic pack. - constexpr bool deny_before_allow_check(const BYTE* types, size_t count) + constexpr bool deny_before_allow_check(const uint8_t* types, size_t count) { bool seenAllow = false; for (size_t i = 0; i < count; ++i) @@ -824,7 +824,7 @@ constexpr auto make_allow_ace(ACCESS_MASK mask, const details::static_sid_t& @param sid A static SID identifying the trustee. */ template -constexpr auto make_allow_ace(BYTE flags, ACCESS_MASK mask, const details::static_sid_t& sid) +constexpr auto make_allow_ace(uint8_t flags, uint32_t mask, const details::static_sid_t& sid) { return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, flags, mask, sid}; } @@ -834,7 +834,7 @@ constexpr auto make_allow_ace(BYTE flags, ACCESS_MASK mask, const details::stati @param sid A static SID identifying the trustee. */ template -constexpr auto make_deny_ace(ACCESS_MASK mask, const details::static_sid_t& sid) +constexpr auto make_deny_ace(uint32_t mask, const details::static_sid_t& sid) { return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, 0, mask, sid}; } @@ -845,7 +845,7 @@ constexpr auto make_deny_ace(ACCESS_MASK mask, const details::static_sid_t& s @param sid A static SID identifying the trustee. */ template -constexpr auto make_deny_ace(BYTE flags, ACCESS_MASK mask, const details::static_sid_t& sid) +constexpr auto make_deny_ace(uint8_t flags, uint32_t mask, const details::static_sid_t& sid) { return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, flags, mask, sid}; } @@ -884,18 +884,18 @@ constexpr auto make_self_relative_sd(const OwnerSid& owner, const GroupSid& grou constexpr size_t totalSize = sdHeaderSize + ownerSize + groupSize + aclHeaderSize + acesSize; // Deny ACEs must precede allow ACEs — will fail constexpr evaluation if violated. - BYTE aceTypes[] = {aces.AceType...}; + uint8_t aceTypes[] = {aces.AceType...}; WI_ASSERT(details::deny_before_allow_check(aceTypes, sizeof...(aces))); details::static_security_descriptor_t result{}; size_t offset = 0; // SECURITY_DESCRIPTOR_RELATIVE header - constexpr WORD control = SE_SELF_RELATIVE | SE_DACL_PRESENT; - constexpr DWORD offsetOwner = (ownerSize > 0) ? static_cast(sdHeaderSize) : 0; - constexpr DWORD offsetGroup = (groupSize > 0) ? static_cast(sdHeaderSize + ownerSize) : 0; - constexpr DWORD offsetSacl = 0; // no SACL - constexpr DWORD offsetDacl = static_cast(sdHeaderSize + ownerSize + groupSize); + constexpr uint16_t control = SE_SELF_RELATIVE | SE_DACL_PRESENT; + constexpr uint32_t offsetOwner = (ownerSize > 0) ? static_cast(sdHeaderSize) : 0; + constexpr uint32_t offsetGroup = (groupSize > 0) ? static_cast(sdHeaderSize + ownerSize) : 0; + constexpr uint32_t offsetSacl = 0; // no SACL + constexpr uint32_t offsetDacl = static_cast(sdHeaderSize + ownerSize + groupSize); details::write_byte(result.data, offset, SECURITY_DESCRIPTOR_REVISION); // Revision details::write_byte(result.data, offset, 0); // Sbz1 @@ -912,8 +912,8 @@ constexpr auto make_self_relative_sd(const OwnerSid& owner, const GroupSid& grou details::write_sid(result.data, offset, group); // DACL: ACL header - constexpr auto aclSize = static_cast(aclHeaderSize + acesSize); - constexpr auto aceCount = static_cast(sizeof...(aces)); + constexpr auto aclSize = static_cast(aclHeaderSize + acesSize); + constexpr auto aceCount = static_cast(sizeof...(aces)); details::write_byte(result.data, offset, ACL_REVISION); // AclRevision details::write_byte(result.data, offset, 0); // Sbz1 details::write_word(result.data, offset, aclSize); // AclSize diff --git a/tests/TokenHelpersTests.cpp b/tests/TokenHelpersTests.cpp index 9d4e88c5f..64506bf5b 100644 --- a/tests/TokenHelpersTests.cpp +++ b/tests/TokenHelpersTests.cpp @@ -366,7 +366,7 @@ TEST_CASE("TokenHelpersTests::SelfRelativeSD_InheritanceFlags", "[token_helpers] wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), wil::no_sid, wil::make_allow_ace( - static_cast(CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE), + static_cast(CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE), GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); From 80a81d37ee8e7edab4345e04c4fe84d4e13f5487 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Wed, 1 Apr 2026 15:47:19 -0700 Subject: [PATCH 4/5] Add consteval make_static_sid<\"S-1-5-18\">() for SID string parsing\n\nParse SID string literals (e.g. \"S-1-5-32-544\") into static_sid_t\nat compile time via C++20 class-type NTTPs. The resulting SID works\nwith all existing helpers: sd_owner, sd_group, make_allow_ace, etc.\n\nGuarded behind __WIL_HAS_CLASS_NTTP which checks for class-type\nNTTP support on both MSVC and Clang." --- include/wil/token_helpers.h | 108 ++++++++++++++++++++++++++++++++++++ tests/TokenHelpersTests.cpp | 40 +++++++++++++ 2 files changed, 148 insertions(+) diff --git a/include/wil/token_helpers.h b/include/wil/token_helpers.h index e861d8bbf..1024ff2c8 100644 --- a/include/wil/token_helpers.h +++ b/include/wil/token_helpers.h @@ -615,9 +615,97 @@ constexpr auto make_static_nt_sid(Ts&&... subAuthorities) #ifdef _HAS_CXX20 +// Class-type NTTPs: MSVC defines __cpp_nontype_template_args; Clang supports it in C++20+ mode without the macro. +#if __cpp_nontype_template_args >= 201911L || (defined(__clang__) && __clang_major__ >= 12 && _HAS_CXX20) +#define __WIL_HAS_CLASS_NTTP 1 +#endif + /// @cond namespace details { +#ifdef __WIL_HAS_CLASS_NTTP + // Fixed-size string for use as a non-type template parameter (C++20). + template + struct fixed_string + { + char data[N]{}; + + consteval fixed_string(const char (&str)[N]) + { + for (size_t i = 0; i < N; ++i) + data[i] = str[i]; + } + + static constexpr size_t length = N - 1; + }; + + // Count '-' characters in a fixed_string. + template + consteval size_t count_dashes() + { + size_t count = 0; + for (size_t i = 0; i < S.length; ++i) + { + if (S.data[i] == '-') + ++count; + } + return count; + } + + // Parse an unsigned decimal integer from a string starting at pos, advancing pos past the digits. + consteval uint64_t parse_uint(const char* str, size_t len, size_t& pos) + { + uint64_t value = 0; + while (pos < len && str[pos] >= '0' && str[pos] <= '9') + { + value = value * 10 + static_cast(str[pos] - '0'); + ++pos; + } + return value; + } + + // Parse a SID string "S-1-{authority}-{sub1}-{sub2}-..." into a static_sid_t. + template + consteval auto parse_sid_string() + { + // "S-R-IA" has 2 dashes; each sub-authority adds one more. + constexpr size_t subAuthCount = count_dashes() - 2; + static_assert(subAuthCount <= SID_MAX_SUB_AUTHORITIES, "too many sub authorities in SID string"); + + static_sid_t result{}; + size_t pos = 0; + + // Expect 'S' or 's' + ++pos; // skip 'S' + ++pos; // skip '-' + + // Revision + result.Revision = static_cast(parse_uint(S.data, S.length, pos)); + ++pos; // skip '-' + + // Identifier authority (big-endian 6-byte value; common authorities fit in the low bytes) + uint64_t authority = parse_uint(S.data, S.length, pos); + result.IdentifierAuthority = {}; + result.IdentifierAuthority.Value[5] = static_cast(authority & 0xFF); + result.IdentifierAuthority.Value[4] = static_cast((authority >> 8) & 0xFF); + result.IdentifierAuthority.Value[3] = static_cast((authority >> 16) & 0xFF); + result.IdentifierAuthority.Value[2] = static_cast((authority >> 24) & 0xFF); + result.IdentifierAuthority.Value[1] = static_cast((authority >> 32) & 0xFF); + result.IdentifierAuthority.Value[0] = static_cast((authority >> 40) & 0xFF); + + result.SubAuthorityCount = static_cast(subAuthCount); + + // Sub-authorities + for (size_t i = 0; i < subAuthCount; ++i) + { + ++pos; // skip '-' + result.SubAuthority[i] = static_cast(parse_uint(S.data, S.length, pos)); + } + + return result; + } +#endif // __WIL_HAS_CLASS_NTTP + // Byte-level constexpr writers for building self-relative security descriptors. constexpr void write_byte(uint8_t* dest, size_t& offset, uint8_t value) { @@ -790,6 +878,26 @@ namespace details //! Sentinel value for omitting the group SID in make_self_relative_sd. inline constexpr details::no_sid_t no_sid{}; +#ifdef __WIL_HAS_CLASS_NTTP +/** Constructs a static SID from a SID string literal at compile time. +The string must be in standard SID notation: "S-{revision}-{authority}-{sub1}-{sub2}-..." +@code +constexpr auto localSystem = wil::make_static_sid<"S-1-5-18">(); +constexpr auto admins = wil::make_static_sid<"S-1-5-32-544">(); +auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_sid<"S-1-5-32-544">()), + wil::sd_group(wil::no_sid), + wil::make_allow_ace(GENERIC_READ, wil::make_static_sid<"S-1-5-11">())); +@endcode +@return A static_sid_t with the parsed SID. +*/ +template +consteval auto make_static_sid() +{ + return details::parse_sid_string(); +} +#endif // __WIL_HAS_CLASS_NTTP + //! Tags a SID as the owner for use with make_self_relative_sd. template constexpr auto sd_owner(const T& sid) diff --git a/tests/TokenHelpersTests.cpp b/tests/TokenHelpersTests.cpp index 64506bf5b..8665b5ccb 100644 --- a/tests/TokenHelpersTests.cpp +++ b/tests/TokenHelpersTests.cpp @@ -422,6 +422,46 @@ TEST_CASE("TokenHelpersTests::SelfRelativeSD_OwnerGroupHelpers", "[token_helpers auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); REQUIRE(EqualSid(pGroup, expectedGroup.get())); } + +#ifdef __WIL_HAS_CLASS_NTTP +TEST_CASE("TokenHelpersTests::SelfRelativeSD_StringSid", "[token_helpers]") +{ + // Parse SID from string literal at compile time + constexpr auto localSystem = wil::make_static_sid<"S-1-5-18">(); + + // Verify sub-authority count and values + auto mutableSid = localSystem; + REQUIRE(IsValidSid(mutableSid.get())); + REQUIRE(*GetSidSubAuthorityCount(mutableSid.get()) == 1); + REQUIRE(*GetSidSubAuthority(mutableSid.get(), 0) == 18); + + // Use string SIDs in make_self_relative_sd via sd_owner + auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_sid<"S-1-5-32-544">()), // BUILTIN\Administrators + wil::sd_group(wil::no_sid), + wil::make_allow_ace(GENERIC_READ, wil::make_static_sid<"S-1-5-11">())); // Authenticated Users + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify owner matches the equivalent numeric SID + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(sd.get(), &pOwner, &ownerDefaulted)); + auto expectedOwner = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + REQUIRE(EqualSid(pOwner, expectedOwner.get())); + + // Verify ACE SID matches the equivalent numeric SID + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + auto expectedAceSid = wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID); + REQUIRE(EqualSid(reinterpret_cast(&static_cast(pAce)->SidStart), expectedAceSid.get())); +} +#endif // __WIL_HAS_CLASS_NTTP + #endif // _HAS_CXX20 #if (_WIN32_WINNT >= _WIN32_WINNT_WIN8) From 421ffee6d0dde638bcd547573403ccb07c7d1bb0 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Wed, 1 Apr 2026 16:11:35 -0700 Subject: [PATCH 5/5] Now with static strings --- include/wil/token_helpers.h | 43 +++++++++++++++++++++++++++++++++++++ tests/TokenHelpersTests.cpp | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/include/wil/token_helpers.h b/include/wil/token_helpers.h index 1024ff2c8..e49ef2c10 100644 --- a/include/wil/token_helpers.h +++ b/include/wil/token_helpers.h @@ -912,6 +912,49 @@ constexpr auto sd_group(const T& sid) return details::sd_group_t{sid}; } +#ifdef __WIL_HAS_CLASS_NTTP +//! Tags a SID parsed from a string literal as the owner. Example: `wil::sd_owner<"S-1-5-18">()` +template +consteval auto sd_owner() +{ + return details::sd_owner_t{details::parse_sid_string()}; +} + +//! Tags a SID parsed from a string literal as the group. Example: `wil::sd_group<"S-1-5-32-545">()` +template +consteval auto sd_group() +{ + return details::sd_group_t{details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_ALLOWED ACE from a SID string literal. +template +consteval auto make_allow_ace(uint32_t mask) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, uint8_t{0}, mask, details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_ALLOWED ACE with inheritance flags from a SID string literal. +template +consteval auto make_allow_ace(uint8_t flags, uint32_t mask) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, flags, mask, details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_DENIED ACE from a SID string literal. +template +consteval auto make_deny_ace(uint32_t mask) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, uint8_t{0}, mask, details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_DENIED ACE with inheritance flags from a SID string literal. +template +consteval auto make_deny_ace(uint8_t flags, uint32_t mask) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, flags, mask, details::parse_sid_string()}; +} +#endif // __WIL_HAS_CLASS_NTTP /** Constructs a constexpr ACCESS_ALLOWED ACE with the given access mask and SID. @code auto ace = wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID)); diff --git a/tests/TokenHelpersTests.cpp b/tests/TokenHelpersTests.cpp index 8665b5ccb..c6a81a99b 100644 --- a/tests/TokenHelpersTests.cpp +++ b/tests/TokenHelpersTests.cpp @@ -460,6 +460,46 @@ TEST_CASE("TokenHelpersTests::SelfRelativeSD_StringSid", "[token_helpers]") auto expectedAceSid = wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID); REQUIRE(EqualSid(reinterpret_cast(&static_cast(pAce)->SidStart), expectedAceSid.get())); } + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_AllStringSyntax", "[token_helpers]") +{ + // Full SD using only string-based SIDs — the most compact form + constexpr auto sd = wil::make_self_relative_sd( + wil::sd_owner<"S-1-5-32-544">(), // BUILTIN\Administrators + wil::sd_group<"S-1-5-32-545">(), // BUILTIN\Users + wil::make_deny_ace<"S-1-5-7">(GENERIC_WRITE), // deny ANONYMOUS LOGON + wil::make_allow_ace<"S-1-5-11">(GENERIC_READ)); // allow Authenticated Users + + auto mutableSd = sd; + REQUIRE(IsValidSecurityDescriptor(mutableSd.get())); + + // Verify owner = S-1-5-32-544 + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(mutableSd.get(), &pOwner, &ownerDefaulted)); + auto expectedOwner = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + REQUIRE(EqualSid(pOwner, expectedOwner.get())); + + // Verify group = S-1-5-32-545 + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(mutableSd.get(), &pGroup, &groupDefaulted)); + auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); + REQUIRE(EqualSid(pGroup, expectedGroup.get())); + + // Verify DACL: deny then allow + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(mutableSd.get(), &daclPresent, &pDacl, &daclDefaulted)); + REQUIRE(pDacl->AceCount == 2); + + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_DENIED_ACE_TYPE); + REQUIRE(GetAce(pDacl, 1, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_ALLOWED_ACE_TYPE); +} #endif // __WIL_HAS_CLASS_NTTP #endif // _HAS_CXX20