< 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
0%
Covered lines: 0
Uncovered lines: 309
Coverable lines: 309
Total lines: 512
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 222
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
Decode(...)0%880%
TryReadTypeSerializationHeader(...)0%16160%
TryReadValidationInfo(...)0%1061060%
FormatRid(...)100%110%
IsEnabledGroup(...)100%110%
TryReadUnicodeStringHeader(...)0%440%
TryReadUnicodeStringData(...)0%16160%
TryReadGroupMemberships(...)0%14140%
TryReadExtraSids(...)0%18180%
TryReadSid(...)0%22220%
.ctor(...)100%110%
.ctor(...)100%110%
.ctor(...)100%110%
TryAlign(...)100%110%
TrySkip(...)0%440%
TryReadByte(...)0%220%
TryReadUInt16(...)0%440%
TryReadUInt32(...)0%440%
TryReadBytes(...)0%440%

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
 041        public string? EffectiveName { get; private set; }
 042        public string? LogonDomainName { get; private set; }
 043        public string? UserSid { get; private set; }
 044        public string? PrimaryGroupSid { get; private set; }
 045        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)
 052        {
 053            NdrReader reader = new NdrReader(buffer);
 54
 055            if (!TryReadTypeSerializationHeader(ref reader))
 056            {
 057                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.
 062            if (!reader.TryReadUInt32(out uint rootReferent) || rootReferent == 0)
 063            {
 064                return null;
 65            }
 66
 067            KerberosPacLogonInfo logonInfo = new KerberosPacLogonInfo();
 068            return logonInfo.TryReadValidationInfo(ref reader) ? logonInfo : null;
 069        }
 70
 71        private static bool TryReadTypeSerializationHeader(ref NdrReader reader)
 072        {
 73            // Common type header, [MS-RPCE] 2.2.6.1.
 074            if (!reader.TryReadByte(out byte version) || version != NdrVersion ||
 075                !reader.TryReadByte(out byte endianness) ||
 076                !reader.TryReadUInt16(out ushort commonHeaderLength) || commonHeaderLength != NdrCommonHeaderLength ||
 077                !reader.TrySkip(4))
 078            {
 079                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.
 084            if ((endianness & 0xF0) != NdrLittleEndian)
 085            {
 086                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.
 091            return reader.TryReadUInt32(out _) && reader.TrySkip(4);
 092        }
 93
 94        private bool TryReadValidationInfo(ref NdrReader reader)
 095        {
 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.
 099            if (!reader.TrySkip(6 * 8))                                  // LogonTime .. PasswordMustChange
 0100            {
 0101                return false;
 102            }
 103
 0104            Span<uint> nameReferents = stackalloc uint[6];
 0105            for (int i = 0; i < nameReferents.Length; i++)
 0106            {
 107                // EffectiveName, FullName, LogonScript, ProfilePath, HomeDirectory, HomeDirectoryDrive
 0108                if (!TryReadUnicodeStringHeader(ref reader, out nameReferents[i]))
 0109                {
 0110                    return false;
 111                }
 0112            }
 113
 0114            if (!reader.TryReadUInt16(out _) ||                          // LogonCount
 0115                !reader.TryReadUInt16(out _) ||                          // BadPasswordCount
 0116                !reader.TryReadUInt32(out uint userId) ||
 0117                !reader.TryReadUInt32(out uint primaryGroupId) ||
 0118                !reader.TryReadUInt32(out uint groupCount) ||
 0119                !reader.TryReadUInt32(out uint groupIdsReferent) ||
 0120                !reader.TryReadUInt32(out _) ||                          // UserFlags
 0121                !reader.TrySkip(16))                                     // UserSessionKey
 0122            {
 0123                return false;
 124            }
 125
 0126            if (!TryReadUnicodeStringHeader(ref reader, out uint logonServerReferent) ||
 0127                !TryReadUnicodeStringHeader(ref reader, out uint logonDomainNameReferent) ||
 0128                !reader.TryReadUInt32(out uint logonDomainIdReferent) ||
 0129                !reader.TrySkip(8) ||                                    // Reserved1[2]
 0130                !reader.TryReadUInt32(out _) ||                          // UserAccountControl
 0131                !reader.TryReadUInt32(out _) ||                          // SubAuthStatus
 0132                !reader.TrySkip(8) ||                                    // LastSuccessfulILogon
 0133                !reader.TrySkip(8) ||                                    // LastFailedILogon
 0134                !reader.TryReadUInt32(out _) ||                          // FailedILogonCount
 0135                !reader.TryReadUInt32(out _) ||                          // Reserved3
 0136                !reader.TryReadUInt32(out uint sidCount) ||
 0137                !reader.TryReadUInt32(out uint extraSidsReferent) ||
 0138                !reader.TryReadUInt32(out uint resourceDomainIdReferent) ||
 0139                !reader.TryReadUInt32(out uint resourceGroupCount) ||
 0140                !reader.TryReadUInt32(out uint resourceGroupIdsReferent))
 0141            {
 0142                return false;
 143            }
 144
 145            // Deferred pointer data, in the order the pointers appear above.
 0146            string?[] names = new string?[nameReferents.Length];
 0147            for (int i = 0; i < nameReferents.Length; i++)
 0148            {
 0149                if (!TryReadUnicodeStringData(ref reader, nameReferents[i], out names[i]))
 0150                {
 0151                    return false;
 152                }
 0153            }
 154
 0155            if (!TryReadGroupMemberships(ref reader, groupIdsReferent, groupCount, out GroupMembership[]? groups) ||
 0156                !TryReadUnicodeStringData(ref reader, logonServerReferent, out _) ||
 0157                !TryReadUnicodeStringData(ref reader, logonDomainNameReferent, out string? logonDomainName) ||
 0158                !TryReadSid(ref reader, logonDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? logonDomainSid) 
 0159                !TryReadExtraSids(ref reader, extraSidsReferent, sidCount, out SidAndAttributes[]? extraSids) ||
 0160                !TryReadSid(ref reader, resourceDomainIdReferent, MaxSidSubAuthorityCount - 1, out string? resourceDomai
 0161                !TryReadGroupMemberships(ref reader, resourceGroupIdsReferent, resourceGroupCount, out GroupMembership[]
 0162            {
 0163                return false;
 164            }
 165
 0166            EffectiveName = names[0];
 0167            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.
 0172            if (logonDomainSid is not null)
 0173            {
 0174                if (userId != 0)
 0175                {
 0176                    UserSid = FormatRid(logonDomainSid, userId);
 0177                }
 178
 0179                if (groups is not null)
 0180                {
 0181                    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                    }
 0194                }
 0195            }
 196
 0197            if (extraSids is not null)
 0198            {
 0199                int firstGroupIndex = 0;
 0200                if (userId == 0)
 0201                {
 0202                    if (extraSids.Length == 0 || extraSids[0].Sid is null)
 0203                    {
 0204                        return false;
 205                    }
 206
 0207                    UserSid = extraSids[0].Sid;
 0208                    firstGroupIndex = 1;
 0209                }
 210
 0211                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                }
 0218            }
 0219            else if (userId == 0)
 0220            {
 0221                return false;
 222            }
 223
 0224            if (resourceDomainSid is not null && resourceGroups is not null)
 0225            {
 0226                foreach (GroupMembership group in resourceGroups)
 0227                {
 0228                    if (IsEnabledGroup(group.Attributes))
 0229                    {
 0230                        GroupSids.Add(FormatRid(resourceDomainSid, group.RelativeId));
 0231                    }
 0232                }
 0233            }
 234
 0235            return true;
 0236        }
 237
 238        private static string FormatRid(string domainSid, uint relativeId) =>
 0239            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)
 0248        {
 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.
 0251            referent = 0;
 0252            return reader.TryReadUInt16(out _) &&
 0253                   reader.TryReadUInt16(out _) &&
 0254                   reader.TryReadUInt32(out referent);
 0255        }
 256
 257        private static bool TryReadUnicodeStringData(ref NdrReader reader, uint referent, out string? value)
 0258        {
 0259            value = null;
 0260            if (referent == 0)
 0261            {
 0262                return true;
 263            }
 264
 265            // Conformant varying array of wchar.
 0266            if (!reader.TryReadUInt32(out uint maxCount) ||
 0267                !reader.TryReadUInt32(out uint offset) ||
 0268                !reader.TryReadUInt32(out uint actualCount) ||
 0269                offset != 0 ||
 0270                actualCount > maxCount)
 0271            {
 0272                return false;
 273            }
 274
 0275            if (actualCount > int.MaxValue / 2 ||
 0276                !reader.TryReadBytes((int)actualCount * 2, out ReadOnlySpan<byte> chars))
 0277            {
 0278                return false;
 279            }
 280
 281            // Some producers include the terminating null in the character count.
 0282            value = Encoding.Unicode.GetString(chars).TrimEnd('\0');
 0283            return true;
 0284        }
 285
 286        private static bool TryReadGroupMemberships(ref NdrReader reader, uint referent, uint count, out GroupMembership
 0287        {
 0288            groups = null;
 0289            if (referent == 0)
 0290            {
 0291                return count == 0;
 292            }
 293
 294            // Conformant array of GROUP_MEMBERSHIP.
 0295            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 0296            {
 0297                return false;
 298            }
 299
 0300            GroupMembership[] result = new GroupMembership[count];
 0301            for (int i = 0; i < result.Length; i++)
 0302            {
 0303                if (!reader.TryReadUInt32(out uint relativeId) ||
 0304                    !reader.TryReadUInt32(out uint attributes))
 0305                {
 0306                    return false;
 307                }
 308
 0309                result[i] = new GroupMembership(relativeId, attributes);
 0310            }
 311
 0312            groups = result;
 0313            return true;
 0314        }
 315
 316        private static bool TryReadExtraSids(ref NdrReader reader, uint referent, uint count, out SidAndAttributes[]? si
 0317        {
 0318            sids = null;
 0319            if (referent == 0)
 0320            {
 0321                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.
 0326            if (!reader.TryReadUInt32(out uint maxCount) || maxCount != count || count > MaxGroupCount)
 0327            {
 0328                return false;
 329            }
 330
 0331            uint[] referents = new uint[count];
 0332            uint[] attributes = new uint[count];
 0333            for (int i = 0; i < referents.Length; i++)
 0334            {
 0335                if (!reader.TryReadUInt32(out referents[i]) ||
 0336                    !reader.TryReadUInt32(out attributes[i]))
 0337                {
 0338                    return false;
 339                }
 0340            }
 341
 0342            SidAndAttributes[] result = new SidAndAttributes[count];
 0343            for (int i = 0; i < referents.Length; i++)
 0344            {
 0345                if (!TryReadSid(ref reader, referents[i], MaxSidSubAuthorityCount, out string? sid))
 0346                {
 0347                    return false;
 348                }
 349
 0350                result[i] = new SidAndAttributes(sid, attributes[i]);
 0351            }
 352
 0353            sids = result;
 0354            return true;
 0355        }
 356
 357        private static bool TryReadSid(ref NdrReader reader, uint referent, int maxSubAuthorityCount, out string? sid)
 0358        {
 0359            sid = null;
 0360            if (referent == 0)
 0361            {
 0362                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.
 0367            if (!reader.TryReadUInt32(out uint maxCount) ||
 0368                !reader.TryReadByte(out byte revision) ||
 0369                !reader.TryReadByte(out byte subAuthorityCount) ||
 0370                !reader.TryReadBytes(6, out ReadOnlySpan<byte> identifierAuthority) ||
 0371                revision != 1 ||
 0372                subAuthorityCount > maxSubAuthorityCount ||
 0373                maxCount != subAuthorityCount)
 0374            {
 0375                return false;
 376            }
 377
 0378            ulong authority = 0;
 0379            foreach (byte b in identifierAuthority)
 0380            {
 0381                authority = (authority << 8) | b;
 0382            }
 383
 0384            StringBuilder builder = new StringBuilder();
 0385            builder.Append("S-").Append(revision).Append('-')
 0386                .Append(authority.ToString(CultureInfo.InvariantCulture));
 387
 0388            for (int i = 0; i < subAuthorityCount; i++)
 0389            {
 0390                if (!reader.TryReadUInt32(out uint subAuthority))
 0391                {
 0392                    return false;
 393                }
 394
 0395                builder.Append('-').Append(subAuthority.ToString(CultureInfo.InvariantCulture));
 0396            }
 397
 0398            sid = builder.ToString();
 0399            return true;
 0400        }
 401
 402        private readonly struct SidAndAttributes
 403        {
 404            public SidAndAttributes(string? sid, uint attributes)
 0405            {
 0406                Sid = sid;
 0407                Attributes = attributes;
 0408            }
 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)
 0417            {
 0418                RelativeId = relativeId;
 0419                Attributes = attributes;
 0420            }
 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)
 0436            {
 0437                _buffer = buffer;
 0438                _position = 0;
 0439            }
 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)
 0446            {
 0447                int padding = (alignment - (_position % alignment)) % alignment;
 0448                return TrySkip(padding);
 0449            }
 450
 451            public bool TrySkip(int count)
 0452            {
 0453                if (count < 0 || _buffer.Length - _position < count)
 0454                {
 0455                    return false;
 456                }
 457
 0458                _position += count;
 0459                return true;
 0460            }
 461
 462            public bool TryReadByte(out byte value)
 0463            {
 0464                if (_position >= _buffer.Length)
 0465                {
 0466                    value = 0;
 0467                    return false;
 468                }
 469
 0470                value = _buffer[_position++];
 0471                return true;
 0472            }
 473
 474            public bool TryReadUInt16(out ushort value)
 0475            {
 0476                value = 0;
 0477                if (!TryAlign(sizeof(ushort)) || !TryReadBytes(sizeof(ushort), out ReadOnlySpan<byte> bytes))
 0478                {
 0479                    return false;
 480                }
 481
 0482                value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
 0483                return true;
 0484            }
 485
 486            public bool TryReadUInt32(out uint value)
 0487            {
 0488                value = 0;
 0489                if (!TryAlign(sizeof(uint)) || !TryReadBytes(sizeof(uint), out ReadOnlySpan<byte> bytes))
 0490                {
 0491                    return false;
 492                }
 493
 0494                value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
 0495                return true;
 0496            }
 497
 498            public bool TryReadBytes(int count, out ReadOnlySpan<byte> value)
 0499            {
 0500                if (count < 0 || _buffer.Length - _position < count)
 0501                {
 0502                    value = default;
 0503                    return false;
 504                }
 505
 0506                value = _buffer.Slice(_position, count);
 0507                _position += count;
 0508                return true;
 0509            }
 510        }
 511    }
 512}