diff --git a/Kerberos.NET/Entities/Krb/KrbExtensions.cs b/Kerberos.NET/Entities/Krb/KrbExtensions.cs new file mode 100644 index 0000000..2f0b9d7 --- /dev/null +++ b/Kerberos.NET/Entities/Krb/KrbExtensions.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------- +// Licensed to The .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ----------------------------------------------------------------------- + +using System.Linq; + +namespace Kerberos.NET.Entities +{ + public static class KrbExtensions + { + public static bool TryGetPac(this KrbEncTicketPart encTicketPart, out PrivilegedAttributeCertificate pac) + { + pac = null; + + KrbAuthorizationData adIfRelevantEntry = encTicketPart.AuthorizationData?.FirstOrDefault(ad => ad.Type == AuthorizationDataType.AdIfRelevant); + if (adIfRelevantEntry == null) + { + return false; + } + + KrbAuthorizationDataSequence adIfRelevant = null; + try + { + adIfRelevant = KrbAuthorizationDataSequence.Decode(adIfRelevantEntry.Data); + } + catch + { + return false; + } + + KrbAuthorizationData pacEntry = adIfRelevant?.AuthorizationData?.First(ad => ad.Type == AuthorizationDataType.AdWin2kPac); + if (pacEntry == null) + { + return false; + } + + try + { + pac = new PrivilegedAttributeCertificate(pacEntry); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/Kerberos.NET/Entities/Krb/KrbKdcRep.cs b/Kerberos.NET/Entities/Krb/KrbKdcRep.cs index d06c4c7..b24a286 100644 --- a/Kerberos.NET/Entities/Krb/KrbKdcRep.cs +++ b/Kerberos.NET/Entities/Krb/KrbKdcRep.cs @@ -271,6 +271,18 @@ private static KrbPrincipalName CreateCNameForTicket(ServiceTicketRequest reques ); } + private static string GetClientNameForPac(ServiceTicketRequest request) + { + // If ClientName is explicitly set, use that for the PAC client name. + // The PAC client name should match the ticket's cname. + if (request.ClientName != null) + { + return request.ClientName.FullyQualifiedName; + } + + return request.Principal.PrincipalName; + } + private static IEnumerable GenerateAuthorizationData(ServiceTicketRequest request) { // authorization-data is annoying because it's a sequence of @@ -303,7 +315,7 @@ private static IEnumerable GenerateAuthorizationData(Servi pac.ClientInformation = new PacClientInfo { ClientId = RpcFileTime.ConvertWithoutMicroseconds(request.Now), - Name = request.Principal.PrincipalName + Name = GetClientNameForPac(request) }; var sequence = new KrbAuthorizationDataSequence diff --git a/Tests/Tests.Kerberos.NET/FakePrincipalService.cs b/Tests/Tests.Kerberos.NET/FakePrincipalService.cs index 9b79da5..f1372d6 100644 --- a/Tests/Tests.Kerberos.NET/FakePrincipalService.cs +++ b/Tests/Tests.Kerberos.NET/FakePrincipalService.cs @@ -30,26 +30,20 @@ public Task FindAsync(KrbPrincipalName principalName, string public IKerberosPrincipal Find(KrbPrincipalName principalName, string realm = null) { - IKerberosPrincipal principal = null; - - bool fallback = false; - if (principalName.FullyQualifiedName.Contains("-fallback", StringComparison.OrdinalIgnoreCase) && principalName.Type == PrincipalNameType.NT_ENTERPRISE) { - principal = null; - fallback = true; + return null; } - if ((principalName.FullyQualifiedName.EndsWith(this.realm, StringComparison.InvariantCultureIgnoreCase) || + if (principalName.FullyQualifiedName.EndsWith(this.realm, StringComparison.InvariantCultureIgnoreCase) || principalName.FullyQualifiedName.StartsWith("krbtgt", StringComparison.InvariantCultureIgnoreCase) || principalName.Type == PrincipalNameType.NT_PRINCIPAL) - && !fallback) { - principal = new FakeKerberosPrincipal(principalName.FullyQualifiedName); + return new FakeKerberosPrincipal(principalName.FullyQualifiedName); } - return principal; + return null; } public X509Certificate2 RetrieveKdcCertificate() diff --git a/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs b/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs index 4783dc9..939858b 100644 --- a/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs +++ b/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography.X509Certificates; using Kerberos.NET; using Kerberos.NET.Client; @@ -12,6 +13,7 @@ using Kerberos.NET.Credentials; using Kerberos.NET.Crypto; using Kerberos.NET.Entities; +using Kerberos.NET.Entities.Pac; using Kerberos.NET.Server; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Tests.Kerberos.NET.KdcListenerTestBase; @@ -33,7 +35,13 @@ public void KdcAsReqHandler_Sync() { KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out _, out KrbAsReq asReq); - ValidateAsRep(asRep, expectedCName: Upn, expectedCRealm: Realm, expectedSRealm: Realm, asReq); + ValidateAsRep( + asRep, + expectedCName: Upn, + expectedCRealm: Realm, + expectedSRealm: Realm, + expectPac: true, + asReq); } [TestMethod] @@ -77,7 +85,8 @@ out KrbEncryptionKey sessionKey expectedCName: Upn, expectedCRealm: Realm, expectedSName: spn, - expectedSRealm: Realm); + expectedSRealm: Realm, + expectPac: true); } [TestMethod] @@ -91,9 +100,21 @@ public void KdcTgsReqHandler_Sync_ReferralTgt() Name = new[] { Upn2WithoutRealm } }; - KrbAsRep asRep = CreateReferralTgt(sourceRealm, destRealm, cname, out KerberosKey tgtKey, out KerberosKey asRepKey, out KrbEncryptionKey sessionKey); + KrbAsRep asRep = CreateReferralTgt( + sourceRealm, + destRealm, + cname, + includePac: true, + out KerberosKey tgtKey, + out KerberosKey asRepKey, + out KrbEncryptionKey sessionKey); - ValidateAsRep(asRep, expectedCName: Upn2WithoutRealm, expectedCRealm: sourceRealm, expectedSRealm: destRealm); + ValidateAsRep( + asRep, + expectedCName: Upn2WithoutRealm, + expectedCRealm: sourceRealm, + expectedSRealm: destRealm, + expectPac: true); // Send a TGS-REQ to get a service ticket in the destination realm var spn = "host/foo." + Realm; @@ -131,10 +152,17 @@ out KrbEncryptionKey subSessionKey expectedCName: Upn2WithoutRealm, expectedCRealm: sourceRealm, expectedSName: spn, - expectedSRealm: destRealm); + expectedSRealm: destRealm, + expectPac: true); } - private void ValidateAsRep(KrbAsRep asRep, string expectedCName, string expectedCRealm, string expectedSRealm, KrbAsReq asReq = null) + private void ValidateAsRep( + KrbAsRep asRep, + string expectedCName, + string expectedCRealm, + string expectedSRealm, + bool expectPac, + KrbAsReq asReq = null) { Assert.IsNotNull(asRep); @@ -170,9 +198,31 @@ private void ValidateAsRep(KrbAsRep asRep, string expectedCName, string expected Assert.IsNotNull(ticketEncPart); Assert.AreEqual(expectedCRealm, ticketEncPart.CRealm); Assert.AreEqual(expectedCName, ticketEncPart.CName.FullyQualifiedName); + + // Check PAC fields + bool success = ticketEncPart.TryGetPac(out PrivilegedAttributeCertificate pac); + if (!expectPac) + { + Assert.IsFalse(success); + Assert.IsNull(pac); + } + else + { + Assert.IsTrue(success); + Assert.IsNotNull(pac); + Assert.AreEqual(expectedCName, pac.ClientInformation.Name); + } } - private void ValidateTgsRep(KrbTgsRep tgsRep, KerberosKey subSessionKey, KerberosKey ticketKey, string expectedCName, string expectedCRealm, string expectedSName, string expectedSRealm) + private void ValidateTgsRep( + KrbTgsRep tgsRep, + KerberosKey subSessionKey, + KerberosKey ticketKey, + string expectedCName, + string expectedCRealm, + string expectedSName, + string expectedSRealm, + bool expectPac) { Assert.IsNotNull(tgsRep); @@ -200,9 +250,30 @@ private void ValidateTgsRep(KrbTgsRep tgsRep, KerberosKey subSessionKey, Kerbero Assert.IsNotNull(ticketEncPart); Assert.AreEqual(expectedCRealm, ticketEncPart.CRealm); Assert.AreEqual(expectedCName, ticketEncPart.CName.FullyQualifiedName); + + // Check PAC fields + bool success = ticketEncPart.TryGetPac(out PrivilegedAttributeCertificate pac); + if (!expectPac) + { + Assert.IsFalse(success); + Assert.IsNull(pac); + } + else + { + Assert.IsTrue(success); + Assert.IsNotNull(pac); + Assert.AreEqual(expectedCName, pac.ClientInformation.Name); + } } - private KrbAsRep CreateReferralTgt(string sourceRealm, string destRealm, KrbPrincipalName cname, out KerberosKey tgtKey, out KerberosKey asRepKey, out KrbEncryptionKey sessionKey) + private KrbAsRep CreateReferralTgt( + string sourceRealm, + string destRealm, + KrbPrincipalName cname, + bool includePac, + out KerberosKey tgtKey, + out KerberosKey asRepKey, + out KrbEncryptionKey sessionKey) { var sourceRealmService = new FakeRealmService(sourceRealm); @@ -217,6 +288,39 @@ private KrbAsRep CreateReferralTgt(string sourceRealm, string destRealm, KrbPrin DateTimeOffset now = DateTimeOffset.UtcNow; + KrbAuthorizationData[] authorizationData = null; + + if (includePac) + { + var pac = clientPrincipal.GeneratePac(); + Assert.IsNotNull(pac); + + pac.ClientInformation = new PacClientInfo + { + Name = cname.FullyQualifiedName, + ClientId = RpcFileTime.ConvertWithoutMicroseconds(now), + }; + + authorizationData = new[] + { + new KrbAuthorizationData + { + Type = AuthorizationDataType.AdIfRelevant, + Data = new KrbAuthorizationDataSequence + { + AuthorizationData = new[] + { + new KrbAuthorizationData + { + Type = AuthorizationDataType.AdWin2kPac, + Data = pac.Encode(tgtKey, tgtKey) + } + } + }.Encode() + } + }; + } + var encTicketPart = new KrbEncTicketPart() { CName = cname, @@ -227,7 +331,7 @@ private KrbAsRep CreateReferralTgt(string sourceRealm, string destRealm, KrbPrin EndTime = now.AddHours(1), RenewTill = now.AddDays(30), Flags = TicketFlags.PreAuthenticated | TicketFlags.Initial | TicketFlags.Renewable | TicketFlags.Forwardable, - AuthorizationData = null, + AuthorizationData = authorizationData, CAddr = new KrbHostAddress[] { }, Transited = new KrbTransitedEncoding() }; diff --git a/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs b/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs index 1265996..1c09f33 100644 --- a/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs +++ b/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs @@ -124,15 +124,15 @@ public void CreateServiceTicket() Compatibility = KerberosCompatibilityFlags.IsolateRealmsConsistently, }); - Assert.IsNotNull(tgsRep); - Assert.AreEqual("blah.com", tgsRep.Ticket.Realm); - Assert.AreEqual("blah@blah.com/blah.com", tgsRep.Ticket.SName.FullyQualifiedName); - Assert.AreEqual("test.com", tgsRep.CRealm); - Assert.AreEqual("blah@test.com", tgsRep.CName.FullyQualifiedName); - - var ticketEncPart = tgsRep.Ticket.EncryptedPart.Decrypt(key, KeyUsage.Ticket, KrbEncTicketPart.DecodeApplication); - Assert.AreEqual("test.com", ticketEncPart.CRealm); - Assert.AreEqual("blah@test.com", ticketEncPart.CName.FullyQualifiedName); + ValidateTgsRep( + tgsRep, + key, + expectedCName: "blah@test.com", + expectedCRealm: "test.com", + expectedSName: "blah@blah.com/blah.com", + expectedSRealm: "blah.com", + expectPac: false, + expectedPacClientName: null); } [TestMethod] @@ -149,17 +149,51 @@ public void CreateServiceTicket_ReferralTgtComputerIdentity() RealmName = "blah.com", ClientRealmName = "test.com", Compatibility = KerberosCompatibilityFlags.IsolateRealmsConsistently, + IncludePac = true, + KdcAuthorizationKey = key }); - Assert.IsNotNull(tgsRep); - Assert.AreEqual("blah.com", tgsRep.Ticket.Realm); - Assert.AreEqual("blah@blah.com/blah.com", tgsRep.Ticket.SName.FullyQualifiedName); - Assert.AreEqual("test.com", tgsRep.CRealm); - Assert.AreEqual("computer$@test.com", tgsRep.CName.FullyQualifiedName); - - var ticketEncPart = tgsRep.Ticket.EncryptedPart.Decrypt(key, KeyUsage.Ticket, KrbEncTicketPart.DecodeApplication); - Assert.AreEqual("test.com", ticketEncPart.CRealm); - Assert.AreEqual("computer$@test.com", ticketEncPart.CName.FullyQualifiedName); + ValidateTgsRep( + tgsRep, + key, + expectedCName: "computer$@test.com", + expectedCRealm: "test.com", + expectedSName: "blah@blah.com/blah.com", + expectedSRealm: "blah.com", + expectPac: true, + // Normally PAC client name should be the same as ticket cname. This is fixed when passing in + // ClientName in the ServiceTicketRequest, see CreateServiceTicket_ReferralTgtComputerIdentity_WithClientName test. + expectedPacClientName: "computer$"); + } + + [TestMethod] + public void CreateServiceTicket_ReferralTgtComputerIdentity_WithClientName() + { + var key = KrbEncryptionKey.Generate(EncryptionType.AES128_CTS_HMAC_SHA1_96).AsKey(); + + var tgsRep = KrbKdcRep.GenerateServiceTicket(new ServiceTicketRequest + { + ClientName = KrbPrincipalName.FromString("computer$@test.com"), // specify client name to get correct PAC client name + EncryptedPartKey = key, + ServicePrincipal = new FakeKerberosPrincipal("blah@blah.com"), + ServicePrincipalKey = key, + Principal = new FakeKerberosPrincipal("computer$"), + RealmName = "blah.com", + ClientRealmName = "test.com", + Compatibility = KerberosCompatibilityFlags.IsolateRealmsConsistently, + IncludePac = true, + KdcAuthorizationKey = key + }); + + ValidateTgsRep( + tgsRep, + key, + expectedCName: "computer$@test.com", + expectedCRealm: "test.com", + expectedSName: "blah@blah.com/blah.com", + expectedSRealm: "blah.com", + expectPac: true, + expectedPacClientName: "computer$@test.com"); } [TestMethod] @@ -183,27 +217,74 @@ public void CreateServiceTicketOnCompatibilitySetting( string expectedCRealm ) { + var cname = $"blah@{crealm}"; + var sname = "blah@blah.com"; + var key = KrbEncryptionKey.Generate(EncryptionType.AES128_CTS_HMAC_SHA1_96).AsKey(); var tgsRep = KrbKdcRep.GenerateServiceTicket(new ServiceTicketRequest { - Principal = new FakeKerberosPrincipal($"blah@{crealm}"), - ClientName = KrbPrincipalName.FromString($"blah@{crealm}"), + Principal = new FakeKerberosPrincipal(cname), + ClientName = KrbPrincipalName.FromString(cname), ClientRealmName = crealm, EncryptedPartKey = key, - ServicePrincipal = new FakeKerberosPrincipal("blah@blah.com"), + ServicePrincipal = new FakeKerberosPrincipal(sname), ServicePrincipalKey = key, RealmName = realm, Compatibility = compatibilityFlags, + + IncludePac = true, + KdcAuthorizationKey = key }); + ValidateTgsRep( + tgsRep, + key, + expectedCName: cname, + expectedCRealm: expectedCRealm, + expectedSName: $"{sname}/{expectedRealm}", + expectedSRealm: expectedRealm, + expectPac: true, + expectedPacClientName: cname); + } + + private void ValidateTgsRep( + KrbTgsRep tgsRep, + KerberosKey ticketKey, + string expectedCName, + string expectedCRealm, + string expectedSName, + string expectedSRealm, + bool expectPac, + string expectedPacClientName) + { Assert.IsNotNull(tgsRep); - Assert.AreEqual(expectedRealm, tgsRep.Ticket.Realm); - var ticketEncPart = tgsRep.Ticket.EncryptedPart.Decrypt(key, KeyUsage.Ticket, KrbEncTicketPart.DecodeApplication); - Assert.AreEqual(expectedCRealm, ticketEncPart.CRealm); + // Check cleartext fields + Assert.AreEqual(expectedCName, tgsRep.CName.FullyQualifiedName); Assert.AreEqual(expectedCRealm, tgsRep.CRealm); + Assert.AreEqual(expectedSName, tgsRep.Ticket.SName.FullyQualifiedName); + Assert.AreEqual(expectedSRealm, tgsRep.Ticket.Realm); + + // Check encrypted ticket fields + var ticketEncPart = tgsRep.Ticket.EncryptedPart.Decrypt(ticketKey, KeyUsage.Ticket, KrbEncTicketPart.DecodeApplication); + Assert.AreEqual(expectedCName, ticketEncPart.CName.FullyQualifiedName); + Assert.AreEqual(expectedCRealm, ticketEncPart.CRealm); + + // Check PAC fields + if (!expectPac) + { + Assert.IsFalse(ticketEncPart.TryGetPac(out _)); + } + else + { + bool success = ticketEncPart.TryGetPac(out PrivilegedAttributeCertificate pac); + Assert.IsTrue(success); + Assert.IsNotNull(pac); + Assert.IsNotNull(pac.ClientInformation); + Assert.AreEqual(expectedPacClientName, pac.ClientInformation.Name); + } } } }