< 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; }
 68545        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)
 70952        {
 70953            NdrReader reader = new NdrReader(buffer);
 54
 70955            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.
 69062            if (!reader.TryReadUInt32(out uint rootReferent) || rootReferent == 0)
 563            {
 564                return null;
 65            }
 66
 68567            KerberosPacLogonInfo logonInfo = new KerberosPacLogonInfo();
 68568            return logonInfo.TryReadValidationInfo(ref reader) ? logonInfo : null;
 70969        }
 70
 71        private static bool TryReadTypeSerializationHeader(ref NdrReader reader)
 70972        {
 73            // Common type header, [MS-RPCE] 2.2.6.1.
 70974            if (!reader.TryReadByte(out byte version) || version != NdrVersion ||
 70975                !reader.TryReadByte(out byte endianness) ||
 70976                !reader.TryReadUInt16(out ushort commonHeaderLength) || commonHeaderLength != NdrCommonHeaderLength ||
 70977                !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.
 70184            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.
 69791            return reader.TryReadUInt32(out _) && reader.TrySkip(4);
 70992        }
 93
 94        private bool TryReadValidationInfo(ref NdrReader reader)
 68595        {
 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.
 68599            if (!reader.TrySkip(6 * 8))                                  // LogonTime .. PasswordMustChange
 5100            {
 5101                return false;
 102            }
 103
 680104            Span<uint> nameReferents = stackalloc uint[6];
 9174105            for (int i = 0; i < nameReferents.Length; i++)
 3947106            {
 107                // EffectiveName, FullName, LogonScript, ProfilePath, HomeDirectory, HomeDirectoryDrive
 3947108                if (!TryReadUnicodeStringHeader(ref reader, out nameReferents[i]))
 40109                {
 40110                    return false;
 111                }
 3907112            }
 113
 640114            if (!reader.TryReadUInt16(out _) ||                          // LogonCount
 640115                !reader.TryReadUInt16(out _) ||                          // BadPasswordCount
 640116                !reader.TryReadUInt32(out uint userId) ||
 640117                !reader.TryReadUInt32(out uint primaryGroupId) ||
 640118                !reader.TryReadUInt32(out uint groupCount) ||
 640119                !reader.TryReadUInt32(out uint groupIdsReferent) ||
 640120                !reader.TryReadUInt32(out _) ||                          // UserFlags
 640121                !reader.TrySkip(16))                                     // UserSessionKey
 14122            {
 14123                return false;
 124            }
 125
 626126            if (!TryReadUnicodeStringHeader(ref reader, out uint logonServerReferent) ||
 626127                !TryReadUnicodeStringHeader(ref reader, out uint logonDomainNameReferent) ||
 626128                !reader.TryReadUInt32(out uint logonDomainIdReferent) ||
 626129                !reader.TrySkip(8) ||                                    // Reserved1[2]
 626130                !reader.TryReadUInt32(out _) ||                          // UserAccountControl
 626131                !reader.TryReadUInt32(out _) ||                          // SubAuthStatus
 626132                !reader.TrySkip(8) ||                                    // LastSuccessfulILogon
 626133                !reader.TrySkip(8) ||                                    // LastFailedILogon
 626134                !reader.TryReadUInt32(out _) ||                          // FailedILogonCount
 626135                !reader.TryReadUInt32(out _) ||                          // Reserved3
 626136                !reader.TryReadUInt32(out uint sidCount) ||
 626137                !reader.TryReadUInt32(out uint extraSidsReferent) ||
 626138                !reader.TryReadUInt32(out uint resourceDomainIdReferent) ||
 626139                !reader.TryReadUInt32(out uint resourceGroupCount) ||
 626140                !reader.TryReadUInt32(out uint resourceGroupIdsReferent))
 60141            {
 60142                return false;
 143            }
 144
 145            // Deferred pointer data, in the order the pointers appear above.
 566146            string?[] names = new string?[nameReferents.Length];
 7380147            for (int i = 0; i < nameReferents.Length; i++)
 3195148            {
 3195149                if (!TryReadUnicodeStringData(ref reader, nameReferents[i], out names[i]))
 71150                {
 71151                    return false;
 152                }
 3124153            }
 154
 495155            if (!TryReadGroupMemberships(ref reader, groupIdsReferent, groupCount, out GroupMembership[]? groups) ||
 495156                !TryReadUnicodeStringData(ref reader, logonServerReferent, out _) ||
 495157                !TryReadUnicodeStringData(ref reader, logonDomainNameReferent, out string? logonDomainName) ||
 495158                !TryReadSid(ref reader, logonDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? logonDomainSid) 
 495159                !TryReadExtraSids(ref reader, extraSidsReferent, sidCount, out SidAndAttributes[]? extraSids) ||
 495160                !TryReadSid(ref reader, resourceDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? resourceDomai
 495161                !TryReadGroupMemberships(ref reader, resourceGroupIdsReferent, resourceGroupCount, out GroupMembership[]
 409162            {
 409163                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;
 685236        }
 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)
 5192248        {
 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.
 5192251            referent = 0;
 5192252            return reader.TryReadUInt16(out _) &&
 5192253                   reader.TryReadUInt16(out _) &&
 5192254                   reader.TryReadUInt32(out referent);
 5192255        }
 256
 257        private static bool TryReadUnicodeStringData(ref NdrReader reader, uint referent, out string? value)
 4035258        {
 4035259            value = null;
 4035260            if (referent == 0)
 3694261            {
 3694262                return true;
 263            }
 264
 265            // Conformant varying array of wchar.
 341266            if (!reader.TryReadUInt32(out uint maxCount) ||
 341267                !reader.TryReadUInt32(out uint offset) ||
 341268                !reader.TryReadUInt32(out uint actualCount) ||
 341269                offset != 0 ||
 341270                actualCount > maxCount)
 69271            {
 69272                return false;
 273            }
 274
 272275            if (actualCount > int.MaxValue / 2 ||
 272276                !reader.TryReadBytes((int)actualCount * 2, out ReadOnlySpan<byte> chars))
 8277            {
 8278                return false;
 279            }
 280
 281            // Some producers include the terminating null in the character count.
 264282            value = Encoding.Unicode.GetString(chars).TrimEnd('\0');
 264283            return true;
 4035284        }
 285
 286        private static bool TryReadGroupMemberships(ref NdrReader reader, uint referent, uint count, out GroupMembership
 597287        {
 597288            groups = null;
 597289            if (referent == 0)
 418290            {
 418291                return count == 0;
 292            }
 293
 294            // Conformant array of GROUP_MEMBERSHIP.
 179295            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 22296            {
 22297                return false;
 298            }
 299
 157300            GroupMembership[] result = new GroupMembership[count];
 4810301            for (int i = 0; i < result.Length; i++)
 2314302            {
 2314303                if (!reader.TryReadUInt32(out uint relativeId) ||
 2314304                    !reader.TryReadUInt32(out uint attributes))
 66305                {
 66306                    return false;
 307                }
 308
 2248309                result[i] = new GroupMembership(relativeId, attributes);
 2248310            }
 311
 91312            groups = result;
 91313            return true;
 597314        }
 315
 316        private static bool TryReadExtraSids(ref NdrReader reader, uint referent, uint count, out SidAndAttributes[]? si
 392317        {
 392318            sids = null;
 392319            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.
 335326            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 23327            {
 23328                return false;
 329            }
 330
 312331            uint[] referents = new uint[count];
 312332            uint[] attributes = new uint[count];
 100672333            for (int i = 0; i < referents.Length; i++)
 50078334            {
 50078335                if (!reader.TryReadUInt32(out referents[i]) ||
 50078336                    !reader.TryReadUInt32(out attributes[i]))
 54337                {
 54338                    return false;
 339                }
 50024340            }
 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;
 392355        }
 356
 357        private static bool TryReadSid(ref NdrReader reader, uint referent, int maxSubAuthorityCount, out string? sid)
 2769358        {
 2769359            sid = null;
 2769360            if (referent == 0)
 2373361            {
 2373362                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;
 2769400        }
 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)
 2248417            {
 2248418                RelativeId = relativeId;
 2248419                Attributes = attributes;
 2248420            }
 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)
 709436            {
 709437                _buffer = buffer;
 709438                _position = 0;
 709439            }
 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)
 134502446            {
 134502447                int padding = (alignment - (_position % alignment)) % alignment;
 134502448                return TrySkip(padding);
 134502449            }
 450
 451            public bool TrySkip(int count)
 139012452            {
 139012453                if (count < 0 || _buffer.Length - _position < count)
 27454                {
 27455                    return false;
 456                }
 457
 138985458                _position += count;
 138985459                return true;
 139012460            }
 461
 462            public bool TryReadByte(out byte value)
 2081463            {
 2081464                if (_position >= _buffer.Length)
 25465                {
 25466                    value = 0;
 25467                    return false;
 468                }
 469
 2056470                value = _buffer[_position++];
 2056471                return true;
 2081472            }
 473
 474            public bool TryReadUInt16(out ushort value)
 12350475            {
 12350476                value = 0;
 12350477                if (!TryAlign(sizeof(ushort)) || !TryReadBytes(sizeof(ushort), out ReadOnlySpan<byte> bytes))
 38478                {
 38479                    return false;
 480                }
 481
 12312482                value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
 12312483                return true;
 12350484            }
 485
 486            public bool TryReadUInt32(out uint value)
 122152487            {
 122152488                value = 0;
 122152489                if (!TryAlign(sizeof(uint)) || !TryReadBytes(sizeof(uint), out ReadOnlySpan<byte> bytes))
 324490                {
 324491                    return false;
 492                }
 493
 121828494                value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
 121828495                return true;
 122152496            }
 497
 498            public bool TryReadBytes(int count, out ReadOnlySpan<byte> value)
 135083499            {
 135083500                if (count < 0 || _buffer.Length - _position < count)
 386501                {
 386502                    value = default;
 386503                    return false;
 504                }
 505
 134697506                value = _buffer.Slice(_position, count);
 134697507                _position += count;
 134697508                return true;
 135083509            }
 510        }
 511    }
 512}