< Summary

Line coverage
95%
Covered lines: 296
Uncovered lines: 13
Coverable lines: 309
Total lines: 513
Line coverage: 95.7%
Branch coverage
95%
Covered branches: 213
Total branches: 222
Branch coverage: 95.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
Decode(...)100%88100%
TryReadTypeSerializationHeader(...)100%1616100%
TryReadValidationInfo(...)94.33%10610689.28%
FormatRid(...)100%11100%
IsEnabledGroup(...)100%11100%
TryReadUnicodeStringHeader(...)100%44100%
TryReadUnicodeStringData(...)100%1616100%
TryReadGroupMemberships(...)100%1414100%
TryReadExtraSids(...)100%1818100%
TryReadSid(...)100%2222100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
TryAlign(...)100%11100%
TrySkip(...)75%44100%
TryReadByte(...)100%22100%
TryReadUInt16(...)75%44100%
TryReadUInt32(...)100%44100%
TryReadBytes(...)75%44100%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/Common/src/System/Net/Security/KerberosPacLogonInfo.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers.Binary;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.Text;
 8
 9namespace System.Net.Security
 10{
 11    /// <summary>
 12    /// Decodes the KERB_VALIDATION_INFO structure carried in the "urn:mspac:logon-info" buffer
 13    /// of a Kerberos Privilege Attribute Certificate (PAC).
 14    /// </summary>
 15    /// <remarks>
 16    /// The structure is serialized using NDR type serialization version 1 as described in
 17    /// [MS-RPCE] 2.2.6, wrapping the KERB_VALIDATION_INFO defined in [MS-PAC] 2.5.
 18    ///
 19    /// Only the fields needed to describe the peer identity are retained. The remaining fields
 20    /// are still parsed, because NDR is a positional format and skipping a field would desync
 21    /// the reader from the stream.
 22    ///
 23    /// Every read is bounds checked and any malformed input results in a null result rather
 24    /// than an exception.
 25    /// </remarks>
 26    internal sealed class KerberosPacLogonInfo
 27    {
 28        // A PAC describing a user with more groups than this is treated as malformed. The value
 29        // is far above what Active Directory can issue, since the resulting ticket would exceed
 30        // the maximum Kerberos token size long before this limit is reached.
 31        private const int MaxGroupCount = 8192;
 32        private const byte MaxSidSubAuthorityCount = 15;
 33        private const uint GroupEnabled = 0x00000004;
 34        private const uint GroupUseForDenyOnly = 0x00000010;
 35        private const uint GroupLogonId = 0xC0000000;
 36
 37        private const byte NdrLittleEndian = 0x10;
 38        private const byte NdrVersion = 1;
 39        private const int NdrCommonHeaderLength = 8;
 40
 21341        public string? EffectiveName { get; private set; }
 21342        public string? LogonDomainName { get; private set; }
 8843        public string? UserSid { get; private set; }
 7244        public string? PrimaryGroupSid { get; private set; }
 83445        public List<string> GroupSids { get; } = new List<string>();
 46
 47        /// <summary>
 48        /// Decodes the logon information, returning null if <paramref name="buffer"/> is not a
 49        /// well formed KERB_VALIDATION_INFO.
 50        /// </summary>
 51        public static KerberosPacLogonInfo? Decode(ReadOnlySpan<byte> buffer)
 70052        {
 70053            NdrReader reader = new NdrReader(buffer);
 54
 70055            if (!TryReadTypeSerializationHeader(ref reader))
 1356            {
 1357                return null;
 58            }
 59
 60            // The root of the serialized type is a unique pointer. A null root carries no
 61            // information and is treated as malformed.
 68762            if (!reader.TryReadUInt32(out uint rootReferent) || rootReferent == 0)
 463            {
 464                return null;
 65            }
 66
 68367            KerberosPacLogonInfo logonInfo = new KerberosPacLogonInfo();
 68368            return logonInfo.TryReadValidationInfo(ref reader) ? logonInfo : null;
 70069        }
 70
 71        private static bool TryReadTypeSerializationHeader(ref NdrReader reader)
 70072        {
 73            // Common type header, [MS-RPCE] 2.2.6.1.
 70074            if (!reader.TryReadByte(out byte version) || version != NdrVersion ||
 70075                !reader.TryReadByte(out byte endianness) ||
 70076                !reader.TryReadUInt16(out ushort commonHeaderLength) || commonHeaderLength != NdrCommonHeaderLength ||
 70077                !reader.TrySkip(4))
 678            {
 679                return false;
 80            }
 81
 82            // Only little-endian integers with an ASCII character set are produced in practice,
 83            // and byte swapping for the alternative has no way of being tested.
 69484            if ((endianness & 0xF0) != NdrLittleEndian)
 285            {
 286                return false;
 87            }
 88
 89            // Private header, [MS-RPCE] 2.2.6.2. The declared length is not trusted for bounds
 90            // checking; the reader validates every access against the real buffer instead.
 69291            return reader.TryReadUInt32(out _) && reader.TrySkip(4);
 70092        }
 93
 94        private bool TryReadValidationInfo(ref NdrReader reader)
 68395        {
 96            // Fixed part of KERB_VALIDATION_INFO, [MS-PAC] 2.5. Pointer fields carry only a
 97            // referent id here; the data they point at is deferred to after the fixed part and
 98            // appears in field order.
 68399            if (!reader.TrySkip(6 * 8))                                  // LogonTime .. PasswordMustChange
 3100            {
 3101                return false;
 102            }
 103
 680104            Span<uint> nameReferents = stackalloc uint[6];
 9278105            for (int i = 0; i < nameReferents.Length; i++)
 3988106            {
 107                // EffectiveName, FullName, LogonScript, ProfilePath, HomeDirectory, HomeDirectoryDrive
 3988108                if (!TryReadUnicodeStringHeader(ref reader, out nameReferents[i]))
 29109                {
 29110                    return false;
 111                }
 3959112            }
 113
 651114            if (!reader.TryReadUInt16(out _) ||                          // LogonCount
 651115                !reader.TryReadUInt16(out _) ||                          // BadPasswordCount
 651116                !reader.TryReadUInt32(out uint userId) ||
 651117                !reader.TryReadUInt32(out uint primaryGroupId) ||
 651118                !reader.TryReadUInt32(out uint groupCount) ||
 651119                !reader.TryReadUInt32(out uint groupIdsReferent) ||
 651120                !reader.TryReadUInt32(out _) ||                          // UserFlags
 651121                !reader.TrySkip(16))                                     // UserSessionKey
 19122            {
 19123                return false;
 124            }
 125
 632126            if (!TryReadUnicodeStringHeader(ref reader, out uint logonServerReferent) ||
 632127                !TryReadUnicodeStringHeader(ref reader, out uint logonDomainNameReferent) ||
 632128                !reader.TryReadUInt32(out uint logonDomainIdReferent) ||
 632129                !reader.TrySkip(8) ||                                    // Reserved1[2]
 632130                !reader.TryReadUInt32(out _) ||                          // UserAccountControl
 632131                !reader.TryReadUInt32(out _) ||                          // SubAuthStatus
 632132                !reader.TrySkip(8) ||                                    // LastSuccessfulILogon
 632133                !reader.TrySkip(8) ||                                    // LastFailedILogon
 632134                !reader.TryReadUInt32(out _) ||                          // FailedILogonCount
 632135                !reader.TryReadUInt32(out _) ||                          // Reserved3
 632136                !reader.TryReadUInt32(out uint sidCount) ||
 632137                !reader.TryReadUInt32(out uint extraSidsReferent) ||
 632138                !reader.TryReadUInt32(out uint resourceDomainIdReferent) ||
 632139                !reader.TryReadUInt32(out uint resourceGroupCount) ||
 632140                !reader.TryReadUInt32(out uint resourceGroupIdsReferent))
 39141            {
 39142                return false;
 143            }
 144
 145            // Deferred pointer data, in the order the pointers appear above.
 593146            string?[] names = new string?[nameReferents.Length];
 7866147            for (int i = 0; i < nameReferents.Length; i++)
 3397148            {
 3397149                if (!TryReadUnicodeStringData(ref reader, nameReferents[i], out names[i]))
 57150                {
 57151                    return false;
 152                }
 3340153            }
 154
 536155            if (!TryReadGroupMemberships(ref reader, groupIdsReferent, groupCount, out GroupMembership[]? groups) ||
 536156                !TryReadUnicodeStringData(ref reader, logonServerReferent, out _) ||
 536157                !TryReadUnicodeStringData(ref reader, logonDomainNameReferent, out string? logonDomainName) ||
 536158                !TryReadSid(ref reader, logonDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? logonDomainSid) 
 536159                !TryReadExtraSids(ref reader, extraSidsReferent, sidCount, out SidAndAttributes[]? extraSids) ||
 536160                !TryReadSid(ref reader, resourceDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? resourceDomai
 536161                !TryReadGroupMemberships(ref reader, resourceGroupIdsReferent, resourceGroupCount, out GroupMembership[]
 323162            {
 323163                return false;
 164            }
 165
 213166            EffectiveName = names[0];
 213167            LogonDomainName = logonDomainName;
 168
 169            // Group membership in the logon domain is expressed as relative identifiers that are
 170            // only meaningful when combined with the domain SID. Without it there is nothing to
 171            // report, but the PAC is still well formed.
 213172            if (logonDomainSid is not null)
 107173            {
 107174                if (userId != 0)
 88175                {
 88176                    UserSid = FormatRid(logonDomainSid, userId);
 88177                }
 178
 107179                if (groups is not null)
 95180                {
 941181                    foreach (GroupMembership group in groups)
 328182                    {
 328183                        if (IsEnabledGroup(group.Attributes))
 151184                        {
 151185                            string groupSid = FormatRid(logonDomainSid, group.RelativeId);
 151186                            GroupSids.Add(groupSid);
 187
 151188                            if (group.RelativeId == primaryGroupId)
 72189                            {
 72190                                PrimaryGroupSid = groupSid;
 72191                            }
 151192                        }
 328193                    }
 95194                }
 107195            }
 196
 213197            if (extraSids is not null)
 57198            {
 57199                int firstGroupIndex = 0;
 57200                if (userId == 0)
 28201                {
 28202                    if (extraSids.Length == 0 || extraSids[0].Sid is null)
 28203                    {
 28204                        return false;
 205                    }
 206
 0207                    UserSid = extraSids[0].Sid;
 0208                    firstGroupIndex = 1;
 0209                }
 210
 92211                for (int i = firstGroupIndex; i < extraSids.Length; i++)
 17212                {
 17213                    if (extraSids[i].Sid is string sid && IsEnabledGroup(extraSids[i].Attributes))
 0214                    {
 0215                        GroupSids.Add(sid);
 0216                    }
 17217                }
 29218            }
 156219            else if (userId == 0)
 33220            {
 33221                return false;
 222            }
 223
 152224            if (resourceDomainSid is not null && resourceGroups is not null)
 14225            {
 42226                foreach (GroupMembership group in resourceGroups)
 0227                {
 0228                    if (IsEnabledGroup(group.Attributes))
 0229                    {
 0230                        GroupSids.Add(FormatRid(resourceDomainSid, group.RelativeId));
 0231                    }
 0232                }
 14233            }
 234
 152235            return true;
 683236        }
 237
 238        private static string FormatRid(string domainSid, uint relativeId) =>
 239239            string.Create(CultureInfo.InvariantCulture, $"{domainSid}-{relativeId}");
 240
 241        private static bool IsEnabledGroup(uint attributes)
 328242        {
 243            const uint RelevantAttributes = GroupEnabled | GroupUseForDenyOnly | GroupLogonId;
 328244            return (attributes & RelevantAttributes) == GroupEnabled;
 328245        }
 246
 247        private static bool TryReadUnicodeStringHeader(ref NdrReader reader, out uint referent)
 5247248        {
 249            // RPC_UNICODE_STRING: the lengths are byte counts and are redundant with the counts
 250            // carried by the deferred conformant varying array, so only the referent is kept.
 5247251            referent = 0;
 5247252            return reader.TryReadUInt16(out _) &&
 5247253                   reader.TryReadUInt16(out _) &&
 5247254                   reader.TryReadUInt32(out referent);
 5247255        }
 256
 257        private static bool TryReadUnicodeStringData(ref NdrReader reader, uint referent, out string? value)
 4345258        {
 4345259            value = null;
 4345260            if (referent == 0)
 3793261            {
 3793262                return true;
 263            }
 264
 265            // Conformant varying array of wchar.
 552266            if (!reader.TryReadUInt32(out uint maxCount) ||
 552267                !reader.TryReadUInt32(out uint offset) ||
 552268                !reader.TryReadUInt32(out uint actualCount) ||
 552269                offset != 0 ||
 552270                actualCount > maxCount)
 67271            {
 67272                return false;
 273            }
 274
 485275            if (actualCount > int.MaxValue / 2 ||
 485276                !reader.TryReadBytes((int)actualCount * 2, out ReadOnlySpan<byte> chars))
 7277            {
 7278                return false;
 279            }
 280
 281            // Some producers include the terminating null in the character count.
 478282            value = Encoding.Unicode.GetString(chars).TrimEnd('\0');
 478283            return true;
 4345284        }
 285
 286        private static bool TryReadGroupMemberships(ref NdrReader reader, uint referent, uint count, out GroupMembership
 774287        {
 774288            groups = null;
 774289            if (referent == 0)
 431290            {
 431291                return count == 0;
 292            }
 293
 294            // Conformant array of GROUP_MEMBERSHIP.
 343295            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 12296            {
 12297                return false;
 298            }
 299
 331300            GroupMembership[] result = new GroupMembership[count];
 4086301            for (int i = 0; i < result.Length; i++)
 1756302            {
 1756303                if (!reader.TryReadUInt32(out uint relativeId) ||
 1756304                    !reader.TryReadUInt32(out uint attributes))
 44305                {
 44306                    return false;
 307                }
 308
 1712309                result[i] = new GroupMembership(relativeId, attributes);
 1712310            }
 311
 287312            groups = result;
 287313            return true;
 774314        }
 315
 316        private static bool TryReadExtraSids(ref NdrReader reader, uint referent, uint count, out SidAndAttributes[]? si
 427317        {
 427318            sids = null;
 427319            if (referent == 0)
 218320            {
 218321                return count == 0;
 322            }
 323
 324            // Conformant array of KERB_SID_AND_ATTRIBUTES. The SID pointers within the array are
 325            // themselves deferred, so the array is read in two passes.
 209326            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 8327            {
 8328                return false;
 329            }
 330
 201331            uint[] referents = new uint[count];
 201332            uint[] attributes = new uint[count];
 5072333            for (int i = 0; i < referents.Length; i++)
 2398334            {
 2398335                if (!reader.TryReadUInt32(out referents[i]) ||
 2398336                    !reader.TryReadUInt32(out attributes[i]))
 63337                {
 63338                    return false;
 339                }
 2335340            }
 341
 138342            SidAndAttributes[] result = new SidAndAttributes[count];
 610343            for (int i = 0; i < referents.Length; i++)
 215344            {
 215345                if (!TryReadSid(ref reader, referents[i], MaxSidSubAuthorityCount, out string? sid))
 48346                {
 48347                    return false;
 348                }
 349
 167350                result[i] = new SidAndAttributes(sid, attributes[i]);
 167351            }
 352
 90353            sids = result;
 90354            return true;
 427355        }
 356
 357        private static bool TryReadSid(ref NdrReader reader, uint referent, int maxSubAuthorityCount, out string? sid)
 984358        {
 984359            sid = null;
 984360            if (referent == 0)
 632361            {
 632362                return true;
 363            }
 364
 365            // RPC_SID is a conformant structure whose maximum count carries the sub authority
 366            // count, duplicating the field inside the structure. Both must agree.
 352367            if (!reader.TryReadUInt32(out uint maxCount) ||
 352368                !reader.TryReadByte(out byte revision) ||
 352369                !reader.TryReadByte(out byte subAuthorityCount) ||
 352370                !reader.TryReadBytes(6, out ReadOnlySpan<byte> identifierAuthority) ||
 352371                revision != 1 ||
 352372                subAuthorityCount > maxSubAuthorityCount ||
 352373                maxCount != subAuthorityCount)
 144374            {
 144375                return false;
 376            }
 377
 208378            ulong authority = 0;
 3120379            foreach (byte b in identifierAuthority)
 1248380            {
 1248381                authority = (authority << 8) | b;
 1248382            }
 383
 208384            StringBuilder builder = new StringBuilder();
 208385            builder.Append("S-").Append(revision).Append('-')
 208386                .Append(authority.ToString(CultureInfo.InvariantCulture));
 387
 712388            for (int i = 0; i < subAuthorityCount; i++)
 156389            {
 156390                if (!reader.TryReadUInt32(out uint subAuthority))
 8391                {
 8392                    return false;
 393                }
 394
 148395                builder.Append('-').Append(subAuthority.ToString(CultureInfo.InvariantCulture));
 148396            }
 397
 200398            sid = builder.ToString();
 200399            return true;
 984400        }
 401
 402        private readonly struct SidAndAttributes
 403        {
 404            public SidAndAttributes(string? sid, uint attributes)
 167405            {
 167406                Sid = sid;
 167407                Attributes = attributes;
 167408            }
 409
 38410            public string? Sid { get; }
 0411            public uint Attributes { get; }
 412        }
 413
 414        private readonly struct GroupMembership
 415        {
 416            public GroupMembership(uint relativeId, uint attributes)
 1712417            {
 1712418                RelativeId = relativeId;
 1712419                Attributes = attributes;
 1712420            }
 421
 302422            public uint RelativeId { get; }
 328423            public uint Attributes { get; }
 424        }
 425
 426        /// <summary>
 427        /// A forward-only reader over an NDR octet stream that fails rather than throwing when
 428        /// the stream is truncated.
 429        /// </summary>
 430        private ref struct NdrReader
 431        {
 432            private readonly ReadOnlySpan<byte> _buffer;
 433            private int _position;
 434
 435            public NdrReader(ReadOnlySpan<byte> buffer)
 700436            {
 700437                _buffer = buffer;
 700438                _position = 0;
 700439            }
 440
 441            // NDR aligns each primitive to its own size relative to the start of the octet
 442            // stream. The type serialization headers that precede the data are 16 bytes, a
 443            // multiple of the largest alignment used here, so offsets within this buffer and
 444            // offsets within the NDR stream agree.
 445            private bool TryAlign(int alignment)
 39227446            {
 39227447                int padding = (alignment - (_position % alignment)) % alignment;
 39227448                return TrySkip(padding);
 39227449            }
 450
 451            public bool TrySkip(int count)
 43773452            {
 43773453                if (count < 0 || _buffer.Length - _position < count)
 17454                {
 17455                    return false;
 456                }
 457
 43756458                _position += count;
 43756459                return true;
 43773460            }
 461
 462            public bool TryReadByte(out byte value)
 2032463            {
 2032464                if (_position >= _buffer.Length)
 10465                {
 10466                    value = 0;
 10467                    return false;
 468                }
 469
 2022470                value = _buffer[_position++];
 2022471                return true;
 2032472            }
 473
 474            public bool TryReadUInt16(out ushort value)
 12477475            {
 12477476                value = 0;
 12477477                if (!TryAlign(sizeof(ushort)) || !TryReadBytes(sizeof(ushort), out ReadOnlySpan<byte> bytes))
 33478                {
 33479                    return false;
 480                }
 481
 12444482                value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
 12444483                return true;
 12477484            }
 485
 486            public bool TryReadUInt32(out uint value)
 26750487            {
 26750488                value = 0;
 26750489                if (!TryAlign(sizeof(uint)) || !TryReadBytes(sizeof(uint), out ReadOnlySpan<byte> bytes))
 260490                {
 260491                    return false;
 492                }
 493
 26490494                value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
 26490495                return true;
 26750496            }
 497
 498            public bool TryReadBytes(int count, out ReadOnlySpan<byte> value)
 40015499            {
 40015500                if (count < 0 || _buffer.Length - _position < count)
 318501                {
 318502                    value = default;
 318503                    return false;
 504                }
 505
 39697506                value = _buffer.Slice(_position, count);
 39697507                _position += count;
 39697508                return true;
 40015509            }
 510        }
 511    }
 512}
 513