< Summary

Information
Class: System.Net.Security.KerberosPacLogonInfo
Assembly: DotnetFuzzing
File(s): D:\runner\runtime\src\libraries\Common\src\System\Net\Security\KerberosPacLogonInfo.cs
Line coverage
88%
Covered lines: 275
Uncovered lines: 34
Coverable lines: 309
Total lines: 512
Line coverage: 88.9%
Branch coverage
91%
Covered branches: 204
Total branches: 222
Branch coverage: 91.8%
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(...)85.84%10610676.78%
FormatRid(...)100%11100%
IsEnabledGroup(...)100%110%
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)

D:\runner\runtime\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
 8641        public string? EffectiveName { get; private set; }
 8642        public string? LogonDomainName { get; private set; }
 2943        public string? UserSid { get; private set; }
 044        public string? PrimaryGroupSid { get; private set; }
 69845        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)
 72252        {
 72253            NdrReader reader = new NdrReader(buffer);
 54
 72255            if (!TryReadTypeSerializationHeader(ref reader))
 1956            {
 1957                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.
 70362            if (!reader.TryReadUInt32(out uint rootReferent) || rootReferent == 0)
 563            {
 564                return null;
 65            }
 66
 69867            KerberosPacLogonInfo logonInfo = new KerberosPacLogonInfo();
 69868            return logonInfo.TryReadValidationInfo(ref reader) ? logonInfo : null;
 72269        }
 70
 71        private static bool TryReadTypeSerializationHeader(ref NdrReader reader)
 72272        {
 73            // Common type header, [MS-RPCE] 2.2.6.1.
 72274            if (!reader.TryReadByte(out byte version) || version != NdrVersion ||
 72275                !reader.TryReadByte(out byte endianness) ||
 72276                !reader.TryReadUInt16(out ushort commonHeaderLength) || commonHeaderLength != NdrCommonHeaderLength ||
 72277                !reader.TrySkip(4))
 878            {
 879                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.
 71484            if ((endianness & 0xF0) != NdrLittleEndian)
 485            {
 486                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.
 71091            return reader.TryReadUInt32(out _) && reader.TrySkip(4);
 72292        }
 93
 94        private bool TryReadValidationInfo(ref NdrReader reader)
 69895        {
 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.
 69899            if (!reader.TrySkip(6 * 8))                                  // LogonTime .. PasswordMustChange
 5100            {
 5101                return false;
 102            }
 103
 693104            Span<uint> nameReferents = stackalloc uint[6];
 9356105            for (int i = 0; i < nameReferents.Length; i++)
 4025106            {
 107                // EffectiveName, FullName, LogonScript, ProfilePath, HomeDirectory, HomeDirectoryDrive
 4025108                if (!TryReadUnicodeStringHeader(ref reader, out nameReferents[i]))
 40109                {
 40110                    return false;
 111                }
 3985112            }
 113
 653114            if (!reader.TryReadUInt16(out _) ||                          // LogonCount
 653115                !reader.TryReadUInt16(out _) ||                          // BadPasswordCount
 653116                !reader.TryReadUInt32(out uint userId) ||
 653117                !reader.TryReadUInt32(out uint primaryGroupId) ||
 653118                !reader.TryReadUInt32(out uint groupCount) ||
 653119                !reader.TryReadUInt32(out uint groupIdsReferent) ||
 653120                !reader.TryReadUInt32(out _) ||                          // UserFlags
 653121                !reader.TrySkip(16))                                     // UserSessionKey
 14122            {
 14123                return false;
 124            }
 125
 639126            if (!TryReadUnicodeStringHeader(ref reader, out uint logonServerReferent) ||
 639127                !TryReadUnicodeStringHeader(ref reader, out uint logonDomainNameReferent) ||
 639128                !reader.TryReadUInt32(out uint logonDomainIdReferent) ||
 639129                !reader.TrySkip(8) ||                                    // Reserved1[2]
 639130                !reader.TryReadUInt32(out _) ||                          // UserAccountControl
 639131                !reader.TryReadUInt32(out _) ||                          // SubAuthStatus
 639132                !reader.TrySkip(8) ||                                    // LastSuccessfulILogon
 639133                !reader.TrySkip(8) ||                                    // LastFailedILogon
 639134                !reader.TryReadUInt32(out _) ||                          // FailedILogonCount
 639135                !reader.TryReadUInt32(out _) ||                          // Reserved3
 639136                !reader.TryReadUInt32(out uint sidCount) ||
 639137                !reader.TryReadUInt32(out uint extraSidsReferent) ||
 639138                !reader.TryReadUInt32(out uint resourceDomainIdReferent) ||
 639139                !reader.TryReadUInt32(out uint resourceGroupCount) ||
 639140                !reader.TryReadUInt32(out uint resourceGroupIdsReferent))
 60141            {
 60142                return false;
 143            }
 144
 145            // Deferred pointer data, in the order the pointers appear above.
 579146            string?[] names = new string?[nameReferents.Length];
 7556147            for (int i = 0; i < nameReferents.Length; i++)
 3272148            {
 3272149                if (!TryReadUnicodeStringData(ref reader, nameReferents[i], out names[i]))
 73150                {
 73151                    return false;
 152                }
 3199153            }
 154
 506155            if (!TryReadGroupMemberships(ref reader, groupIdsReferent, groupCount, out GroupMembership[]? groups) ||
 506156                !TryReadUnicodeStringData(ref reader, logonServerReferent, out _) ||
 506157                !TryReadUnicodeStringData(ref reader, logonDomainNameReferent, out string? logonDomainName) ||
 506158                !TryReadSid(ref reader, logonDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? logonDomainSid) 
 506159                !TryReadExtraSids(ref reader, extraSidsReferent, sidCount, out SidAndAttributes[]? extraSids) ||
 506160                !TryReadSid(ref reader, resourceDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? resourceDomai
 506161                !TryReadGroupMemberships(ref reader, resourceGroupIdsReferent, resourceGroupCount, out GroupMembership[]
 420162            {
 420163                return false;
 164            }
 165
 86166            EffectiveName = names[0];
 86167            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.
 86172            if (logonDomainSid is not null)
 61173            {
 61174                if (userId != 0)
 29175                {
 29176                    UserSid = FormatRid(logonDomainSid, userId);
 29177                }
 178
 61179                if (groups is not null)
 31180                {
 93181                    foreach (GroupMembership group in groups)
 0182                    {
 0183                        if (IsEnabledGroup(group.Attributes))
 0184                        {
 0185                            string groupSid = FormatRid(logonDomainSid, group.RelativeId);
 0186                            GroupSids.Add(groupSid);
 187
 0188                            if (group.RelativeId == primaryGroupId)
 0189                            {
 0190                                PrimaryGroupSid = groupSid;
 0191                            }
 0192                        }
 0193                    }
 31194                }
 61195            }
 196
 86197            if (extraSids is not null)
 57198            {
 57199                int firstGroupIndex = 0;
 57200                if (userId == 0)
 24201                {
 24202                    if (extraSids.Length == 0 || extraSids[0].Sid is null)
 24203                    {
 24204                        return false;
 205                    }
 206
 0207                    UserSid = extraSids[0].Sid;
 0208                    firstGroupIndex = 1;
 0209                }
 210
 66211                for (int i = firstGroupIndex; i < extraSids.Length; i++)
 0212                {
 0213                    if (extraSids[i].Sid is string sid && IsEnabledGroup(extraSids[i].Attributes))
 0214                    {
 0215                        GroupSids.Add(sid);
 0216                    }
 0217                }
 33218            }
 29219            else if (userId == 0)
 23220            {
 23221                return false;
 222            }
 223
 39224            if (resourceDomainSid is not null && resourceGroups is not null)
 13225            {
 39226                foreach (GroupMembership group in resourceGroups)
 0227                {
 0228                    if (IsEnabledGroup(group.Attributes))
 0229                    {
 0230                        GroupSids.Add(FormatRid(resourceDomainSid, group.RelativeId));
 0231                    }
 0232                }
 13233            }
 234
 39235            return true;
 698236        }
 237
 238        private static string FormatRid(string domainSid, uint relativeId) =>
 29239            string.Create(CultureInfo.InvariantCulture, $"{domainSid}-{relativeId}");
 240
 241        private static bool IsEnabledGroup(uint attributes)
 0242        {
 243            const uint RelevantAttributes = GroupEnabled | GroupUseForDenyOnly | GroupLogonId;
 0244            return (attributes & RelevantAttributes) == GroupEnabled;
 0245        }
 246
 247        private static bool TryReadUnicodeStringHeader(ref NdrReader reader, out uint referent)
 5296248        {
 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.
 5296251            referent = 0;
 5296252            return reader.TryReadUInt16(out _) &&
 5296253                   reader.TryReadUInt16(out _) &&
 5296254                   reader.TryReadUInt32(out referent);
 5296255        }
 256
 257        private static bool TryReadUnicodeStringData(ref NdrReader reader, uint referent, out string? value)
 4114258        {
 4114259            value = null;
 4114260            if (referent == 0)
 3770261            {
 3770262                return true;
 263            }
 264
 265            // Conformant varying array of wchar.
 344266            if (!reader.TryReadUInt32(out uint maxCount) ||
 344267                !reader.TryReadUInt32(out uint offset) ||
 344268                !reader.TryReadUInt32(out uint actualCount) ||
 344269                offset != 0 ||
 344270                actualCount > maxCount)
 70271            {
 70272                return false;
 273            }
 274
 274275            if (actualCount > int.MaxValue / 2 ||
 274276                !reader.TryReadBytes((int)actualCount * 2, out ReadOnlySpan<byte> chars))
 9277            {
 9278                return false;
 279            }
 280
 281            // Some producers include the terminating null in the character count.
 265282            value = Encoding.Unicode.GetString(chars).TrimEnd('\0');
 265283            return true;
 4114284        }
 285
 286        private static bool TryReadGroupMemberships(ref NdrReader reader, uint referent, uint count, out GroupMembership
 608287        {
 608288            groups = null;
 608289            if (referent == 0)
 419290            {
 419291                return count == 0;
 292            }
 293
 294            // Conformant array of GROUP_MEMBERSHIP.
 189295            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 22296            {
 22297                return false;
 298            }
 299
 167300            GroupMembership[] result = new GroupMembership[count];
 5202301            for (int i = 0; i < result.Length; i++)
 2510302            {
 2510303                if (!reader.TryReadUInt32(out uint relativeId) ||
 2510304                    !reader.TryReadUInt32(out uint attributes))
 76305                {
 76306                    return false;
 307                }
 308
 2434309                result[i] = new GroupMembership(relativeId, attributes);
 2434310            }
 311
 91312            groups = result;
 91313            return true;
 608314        }
 315
 316        private static bool TryReadExtraSids(ref NdrReader reader, uint referent, uint count, out SidAndAttributes[]? si
 393317        {
 393318            sids = null;
 393319            if (referent == 0)
 57320            {
 57321                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.
 336326            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 23327            {
 23328                return false;
 329            }
 330
 313331            uint[] referents = new uint[count];
 313332            uint[] attributes = new uint[count];
 100680333            for (int i = 0; i < referents.Length; i++)
 50082334            {
 50082335                if (!reader.TryReadUInt32(out referents[i]) ||
 50082336                    !reader.TryReadUInt32(out attributes[i]))
 55337                {
 55338                    return false;
 339                }
 50027340            }
 341
 258342            SidAndAttributes[] result = new SidAndAttributes[count];
 4592343            for (int i = 0; i < referents.Length; i++)
 2225344            {
 2225345                if (!TryReadSid(ref reader, referents[i], MaxSidSubAuthorityCount, out string? sid))
 187346                {
 187347                    return false;
 348                }
 349
 2038350                result[i] = new SidAndAttributes(sid, attributes[i]);
 2038351            }
 352
 71353            sids = result;
 71354            return true;
 393355        }
 356
 357        private static bool TryReadSid(ref NdrReader reader, uint referent, int maxSubAuthorityCount, out string? sid)
 2770358        {
 2770359            sid = null;
 2770360            if (referent == 0)
 2374361            {
 2374362                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.
 396367            if (!reader.TryReadUInt32(out uint maxCount) ||
 396368                !reader.TryReadByte(out byte revision) ||
 396369                !reader.TryReadByte(out byte subAuthorityCount) ||
 396370                !reader.TryReadBytes(6, out ReadOnlySpan<byte> identifierAuthority) ||
 396371                revision != 1 ||
 396372                subAuthorityCount > maxSubAuthorityCount ||
 396373                maxCount != subAuthorityCount)
 236374            {
 236375                return false;
 376            }
 377
 160378            ulong authority = 0;
 2400379            foreach (byte b in identifierAuthority)
 960380            {
 960381                authority = (authority << 8) | b;
 960382            }
 383
 160384            StringBuilder builder = new StringBuilder();
 160385            builder.Append("S-").Append(revision).Append('-')
 160386                .Append(authority.ToString(CultureInfo.InvariantCulture));
 387
 334388            for (int i = 0; i < subAuthorityCount; i++)
 8389            {
 8390                if (!reader.TryReadUInt32(out uint subAuthority))
 1391                {
 1392                    return false;
 393                }
 394
 7395                builder.Append('-').Append(subAuthority.ToString(CultureInfo.InvariantCulture));
 7396            }
 397
 159398            sid = builder.ToString();
 159399            return true;
 2770400        }
 401
 402        private readonly struct SidAndAttributes
 403        {
 404            public SidAndAttributes(string? sid, uint attributes)
 2038405            {
 2038406                Sid = sid;
 2038407                Attributes = attributes;
 2038408            }
 409
 0410            public string? Sid { get; }
 0411            public uint Attributes { get; }
 412        }
 413
 414        private readonly struct GroupMembership
 415        {
 416            public GroupMembership(uint relativeId, uint attributes)
 2434417            {
 2434418                RelativeId = relativeId;
 2434419                Attributes = attributes;
 2434420            }
 421
 0422            public uint RelativeId { get; }
 0423            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)
 722436            {
 722437                _buffer = buffer;
 722438                _position = 0;
 722439            }
 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)
 135485446            {
 135485447                int padding = (alignment - (_position % alignment)) % alignment;
 135485448                return TrySkip(padding);
 135485449            }
 450
 451            public bool TrySkip(int count)
 140086452            {
 140086453                if (count < 0 || _buffer.Length - _position < count)
 28454                {
 28455                    return false;
 456                }
 457
 140058458                _position += count;
 140058459                return true;
 140086460            }
 461
 462            public bool TryReadByte(out byte value)
 2107463            {
 2107464                if (_position >= _buffer.Length)
 25465                {
 25466                    value = 0;
 25467                    return false;
 468                }
 469
 2082470                value = _buffer[_position++];
 2082471                return true;
 2107472            }
 473
 474            public bool TryReadUInt16(out ushort value)
 12597475            {
 12597476                value = 0;
 12597477                if (!TryAlign(sizeof(ushort)) || !TryReadBytes(sizeof(ushort), out ReadOnlySpan<byte> bytes))
 38478                {
 38479                    return false;
 480                }
 481
 12559482                value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
 12559483                return true;
 12597484            }
 485
 486            public bool TryReadUInt32(out uint value)
 122888487            {
 122888488                value = 0;
 122888489                if (!TryAlign(sizeof(uint)) || !TryReadBytes(sizeof(uint), out ReadOnlySpan<byte> bytes))
 336490                {
 336491                    return false;
 492                }
 493
 122552494                value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
 122552495                return true;
 122888496            }
 497
 498            public bool TryReadBytes(int count, out ReadOnlySpan<byte> value)
 136066499            {
 136066500                if (count < 0 || _buffer.Length - _position < count)
 397501                {
 397502                    value = default;
 397503                    return false;
 504                }
 505
 135669506                value = _buffer.Slice(_position, count);
 135669507                _position += count;
 135669508                return true;
 136066509            }
 510        }
 511    }
 512}