diff --git a/Kerberos.NET/Entities/Krb/KrbKdcRep.cs b/Kerberos.NET/Entities/Krb/KrbKdcRep.cs index 7eafcfd8..d06c4c71 100644 --- a/Kerberos.NET/Entities/Krb/KrbKdcRep.cs +++ b/Kerberos.NET/Entities/Krb/KrbKdcRep.cs @@ -194,8 +194,6 @@ private static KrbEncTicketPart CreateEncTicketPart( KrbEncryptionKey sessionKey ) { - var cname = CreateCNameForTicket(request); - var flags = request.Flags; if (request.PreAuthenticationData?.Any(r => r.Type == PaDataType.PA_REQ_ENC_PA_REP) ?? false) @@ -209,7 +207,7 @@ KrbEncryptionKey sessionKey var encTicketPart = new KrbEncTicketPart() { - CName = cname, + CName = CreateCNameForTicket(request), CRealm = request.Compatibility.HasFlag(KerberosCompatibilityFlags.IsolateRealmsConsistently) ? request.ClientRealmName : request.RealmName, Key = sessionKey, AuthTime = request.Now, @@ -233,21 +231,44 @@ KrbEncryptionKey sessionKey private static KrbPrincipalName CreateCNameForTicket(ServiceTicketRequest request) { - if (string.IsNullOrEmpty(request.SamAccountName)) + // If ClientName is explicitly set, use that. This is the preferred method for the caller to indicate what + // cname should be used. + if (request.ClientName != null) { - return KrbPrincipalName.FromPrincipal( - request.Principal, - realm: request.Compatibility.HasFlag(KerberosCompatibilityFlags.IsolateRealmsConsistently) ? - request.ClientRealmName : - request.RealmName - ); + return request.ClientName; } - return new KrbPrincipalName + // Otherwise, if SamAccountName is set, use that as the principal name. + // This is not the recommended method, but is supported for backwards compatibility. +#pragma warning disable CS0618 // Type or member is obsolete + if (!string.IsNullOrEmpty(request.SamAccountName)) { - Type = PrincipalNameType.NT_PRINCIPAL, - Name = new[] { request.SamAccountName } - }; + // Note that the name is returned in a single part here, even if the request may have had multiple parts. + // This may be okay for AS-REQs with Canonicalize set, but it is not spec-compliant for other scenarios. + return new KrbPrincipalName + { + Type = PrincipalNameType.NT_PRINCIPAL, + Name = new[] { request.SamAccountName } + }; + } +#pragma warning restore CS0618 // Type or member is obsolete + + // Lastly, if neither are set, derive from the principal. + // + // Note: this might not be correct in all scenarios. For instance, if the client does not accept + // name canonicalization (i.e., the Canonicalize flag is not set), then it's not spec-compliant to deviate + // from the requested cname. Also, in TGS-REP, the cname should match what's in the TGT, and should not be + // derived from the found principal. It is the responsibility of the caller to decide whether the request + // warrants passing Principal only, or forcing a specific cname via ClientName. + // + // Note: historically Kerberos.NET had a bug where the service realm was used to derive cname from the principal. + // However, it should be the client realm. This has been corrected under the IsolateRealmsConsistently flag. + return KrbPrincipalName.FromPrincipal( + request.Principal, + realm: request.Compatibility.HasFlag(KerberosCompatibilityFlags.IsolateRealmsConsistently) ? + request.ClientRealmName : + request.RealmName + ); } private static IEnumerable GenerateAuthorizationData(ServiceTicketRequest request) diff --git a/Kerberos.NET/Entities/Krb/ServiceTicketRequest.cs b/Kerberos.NET/Entities/Krb/ServiceTicketRequest.cs index 611e9b52..0f7a8cf5 100644 --- a/Kerberos.NET/Entities/Krb/ServiceTicketRequest.cs +++ b/Kerberos.NET/Entities/Krb/ServiceTicketRequest.cs @@ -28,7 +28,13 @@ public struct ServiceTicketRequest : IEquatable public KerberosKey KdcAuthorizationKey { get; set; } /// - /// The realm name for which the requested identity originated + /// The client name (cname) of the identity requesting the ticket. + /// If this is not set, or will be used. + /// + public KrbPrincipalName ClientName { get; set; } + + /// + /// The client realm (crealm) name of the identity requesting the ticket /// public string ClientRealmName { get; set; } @@ -110,9 +116,12 @@ public struct ServiceTicketRequest : IEquatable /// /// SAM account name to be used to generate TGT for Windows specific user principal. - /// If this parameter contains valid string (not empty), CName of encrypted part of ticket - /// will be created based on provided SamAccountName. + /// This is only used if (1) is not set, and (2) it is a valid (not empty) string. + /// Used to compute the cname of a KDC-REP. /// + [Obsolete( + "Using SamAccountName may cause non spec-compliant behavior. Use ClientName instead to set the client " + + "principal name that should be used in Kerberos responses in a spec-compliant manner.")] public string SamAccountName { get; set; } /// @@ -179,6 +188,21 @@ public bool Equals(ServiceTicketRequest other) return false; } + if (other.ClientName != this.ClientName) + { + return false; + } + + if (other.ClientRealmName != this.ClientRealmName) + { + return false; + } + + if (other.EncryptedPartEType != this.EncryptedPartEType) + { + return false; + } + if (other.EncryptedPartKey != this.EncryptedPartKey) { return false; @@ -249,10 +273,12 @@ public bool Equals(ServiceTicketRequest other) return false; } +#pragma warning disable CS0618 // Type or member is obsolete if (other.SamAccountName != this.SamAccountName) { return false; } +#pragma warning restore CS0618 // Type or member is obsolete if (other.ServicePrincipal != this.ServicePrincipal) { @@ -279,8 +305,13 @@ public bool Equals(ServiceTicketRequest other) public override int GetHashCode() { +#pragma warning disable CS0618 // Type or member is obsolete return EntityHashCode.GetHashCode( this.Addresses, + this.ClientName, + this.ClientRealmName, + this.Compatibility, + this.EncryptedPartEType, this.EncryptedPartKey, this.EndTime, this.Flags, @@ -301,6 +332,7 @@ public override int GetHashCode() this.StartTime, this.Compatibility ); +#pragma warning restore CS0618 // Type or member is obsolete } public static bool operator ==(ServiceTicketRequest left, ServiceTicketRequest right) diff --git a/Kerberos.NET/Server/KdcAsReqMessageHandler.cs b/Kerberos.NET/Server/KdcAsReqMessageHandler.cs index f2449fe8..e2e57ea5 100644 --- a/Kerberos.NET/Server/KdcAsReqMessageHandler.cs +++ b/Kerberos.NET/Server/KdcAsReqMessageHandler.cs @@ -167,6 +167,7 @@ private ReadOnlyMemory GenerateAsRep(KrbAsReq asReq, PreAuthenticationCont var rst = new ServiceTicketRequest { + ClientRealmName = asReq.Body.Realm, Principal = context.Principal, EncryptedPartKey = context.EncryptedPartKey, EncryptedPartEType = context.EncryptedPartEType, @@ -193,10 +194,30 @@ private ReadOnlyMemory GenerateAsRep(KrbAsReq asReq, PreAuthenticationCont // Canonicalize means the CName in the reply is allowed to be different from the CName in the request. // If this is not allowed, then we must use the CName from the request. Otherwise, we will set the CName - // to what we have in our realm, i.e. user@realm. + // to what we have in our realm, i.e. user@realm (which will be inferred from the Principal set above). + // + // RFC 4120 section 3.1.5. Receipt of KRB_AS_REP Message + // ----------------------------------------------------- + // If the reply message type is KRB_AS_REP, then the client verifies that the cname and crealm fields in + // the cleartext portion of the reply match what it requested. + // + // RFC 6806 section 6. Name Canonicalization + // ----------------------------------------- + // If the "canonicalize" KDC option is set, then the KDC MAY change the client and server principal names + // and types in the AS response and ticket returned from those in the request. if (!asReq.Body.KdcOptions.HasFlag(KdcOptions.Canonicalize)) { - rst.SamAccountName = asReq.Body.CName.FullyQualifiedName; + if (this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.EnableSpecCompliantCNameHandling)) + { + rst.ClientName = asReq.Body.CName; + } + else + { + #pragma warning disable CS0618 // Type or member is obsolete + rst.SamAccountName = asReq.Body.CName.FullyQualifiedName; + #pragma warning restore CS0618 // Type or member is obsolete + } + } if (rst.EncryptedPartKey == null) diff --git a/Kerberos.NET/Server/KdcTgsReqMessageHandler.cs b/Kerberos.NET/Server/KdcTgsReqMessageHandler.cs index 5c82ca0a..ded21031 100644 --- a/Kerberos.NET/Server/KdcTgsReqMessageHandler.cs +++ b/Kerberos.NET/Server/KdcTgsReqMessageHandler.cs @@ -265,13 +265,13 @@ public override ReadOnlyMemory ExecuteCore(PreAuthenticationContext contex var rst = new ServiceTicketRequest { + ClientRealmName = context.Ticket.CRealm, KdcAuthorizationKey = context.EvidenceTicketKey, Principal = context.Principal, EncryptedPartKey = context.EncryptedPartKey, EncryptedPartEType = context.EncryptedPartEType, ServicePrincipal = context.ServicePrincipal, ServicePrincipalKey = serviceKey, - ClientRealmName = context.ClientRealm, RealmName = tgsReq.Body.Realm, Addresses = tgsReq.Body.Addresses, RenewTill = context.Ticket.RenewTill, @@ -290,13 +290,35 @@ public override ReadOnlyMemory ExecuteCore(PreAuthenticationContext contex Compatibility = this.RealmService.Settings.Compatibility, }; - // this introduced an annoying regression in a separate party and this is a workaround to make sure it - // uses the original behavior in cases where that's expected - - if (!this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.DoNotCanonicalizeTgsReqFromTgt) && + if (this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.EnableSpecCompliantCNameHandling)) + { + // In TGS-REP, we should always reply with cname/crealm copied from the TGT, even when the Canonicalize + // flag is set in TGS-REQ. + // + // RFC 4120 § 3.3.3 Generation of KRB_TGS_REP Message + // -------------------------------------------------- + // "By default, the address field, the client's name and realm, the list of transited realms, the time + // of initial authentication, the expiration time, and the authorization data of the newly-issued + // ticket will be copied from the TGT or renewable ticket." + // + // RFC 6806 § 6. Name Canonicalization + // ----------------------------------- + // "If the "canonicalize" KDC option is set, then the KDC MAY change the client and server principal + // names and types in the AS response and ticket returned from those in the request. Names MUST NOT be + // changed in the response to a TGS request, although it is common for KDCs to maintain a set of + // aliases for service principals." + rst.ClientName = context.Ticket.CName; + } + else if (!this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.DoNotCanonicalizeTgsReqFromTgt) && tgsReq.Body.KdcOptions.HasFlag(KdcOptions.Canonicalize)) { + // The code below introduced an annoying regression in a separate party. + // The compatibility flag is a workaround to make sure it can use the original behavior in cases where + // that's expected. + + #pragma warning disable CS0612 // Type or member is obsolete rst.SamAccountName = context.GetState(PaDataType.PA_TGS_REQ).DecryptedApReq.Ticket.CName.FullyQualifiedName; + #pragma warning restore CS0612 // Type or member is obsolete } // this is set here instead of in GenerateServiceTicket because GST is used by unit tests to diff --git a/Kerberos.NET/Server/KerberosCompatibilityFlags.cs b/Kerberos.NET/Server/KerberosCompatibilityFlags.cs index 7323aaeb..1ab58f62 100644 --- a/Kerberos.NET/Server/KerberosCompatibilityFlags.cs +++ b/Kerberos.NET/Server/KerberosCompatibilityFlags.cs @@ -33,5 +33,12 @@ public enum KerberosCompatibilityFlags /// fields or properties. This separates the names into two. /// IsolateRealmsConsistently = 1 << 2, + + /// + /// CName handling was historically non-spec compliant in some cases. + /// This flag enables handling that more strictly adheres to the spec, for better compliance + /// with other implementations. + /// + EnableSpecCompliantCNameHandling = 1 << 3, } } diff --git a/Kerberos.NET/Server/PaDataTgsTicketHandler.cs b/Kerberos.NET/Server/PaDataTgsTicketHandler.cs index 3132ae44..1e1a41f8 100644 --- a/Kerberos.NET/Server/PaDataTgsTicketHandler.cs +++ b/Kerberos.NET/Server/PaDataTgsTicketHandler.cs @@ -105,7 +105,6 @@ public override KrbPaData Validate(KrbKdcReq asReq, PreAuthenticationContext con context.EncryptedPartKey = state.DecryptedApReq.SessionKey; context.Ticket = state.DecryptedApReq.Ticket; - context.ClientRealm = state.DecryptedApReq.Ticket.CRealm; return null; } diff --git a/Kerberos.NET/Server/PreAuthenticationContext.cs b/Kerberos.NET/Server/PreAuthenticationContext.cs index da6c3d4f..c729f03d 100644 --- a/Kerberos.NET/Server/PreAuthenticationContext.cs +++ b/Kerberos.NET/Server/PreAuthenticationContext.cs @@ -32,11 +32,6 @@ public class PreAuthenticationContext /// public KerberosKey EvidenceTicketKey { get; set; } - /// - /// The name of the realm that the client issued a TGT from. - /// - public string ClientRealm { get; set; } - /// /// The identity that will be the subject of the issued ticket. /// diff --git a/Tests/Tests.Kerberos.NET/Client/HttpKerberosTransportTests.cs b/Tests/Tests.Kerberos.NET/Client/HttpKerberosTransportTests.cs index b2fcb145..78e3ab56 100644 --- a/Tests/Tests.Kerberos.NET/Client/HttpKerberosTransportTests.cs +++ b/Tests/Tests.Kerberos.NET/Client/HttpKerberosTransportTests.cs @@ -92,6 +92,8 @@ protected override Task SendAsync(HttpRequestMessage reques var rst = new ServiceTicketRequest { + ClientName = KrbPrincipalName.FromPrincipal(principal), + ClientRealmName = Realm, Principal = principal, EncryptedPartKey = principalKey, ServicePrincipalKey = new KerberosKey(key: TgtKey, etype: EncryptionType.AES256_CTS_HMAC_SHA1_96) diff --git a/Tests/Tests.Kerberos.NET/FakeRealmService.cs b/Tests/Tests.Kerberos.NET/FakeRealmService.cs index 9ae0e7e3..77a95d75 100644 --- a/Tests/Tests.Kerberos.NET/FakeRealmService.cs +++ b/Tests/Tests.Kerberos.NET/FakeRealmService.cs @@ -13,7 +13,7 @@ public class FakeRealmService : IRealmService { private readonly KerberosCompatibilityFlags compatibilityFlags; - public FakeRealmService(string realm, Krb5Config config = null, KerberosCompatibilityFlags compatibilityFlags = KerberosCompatibilityFlags.IsolateRealmsConsistently) + public FakeRealmService(string realm, Krb5Config config = null, KerberosCompatibilityFlags compatibilityFlags = KerberosCompatibilityFlags.IsolateRealmsConsistently | KerberosCompatibilityFlags.EnableSpecCompliantCNameHandling) { this.Name = realm; this.Configuration = config ?? Krb5Config.Kdc(); diff --git a/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs b/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs index 40dcfd2a..4783dc96 100644 --- a/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs +++ b/Tests/Tests.Kerberos.NET/Kdc/KdcHandlerTests.cs @@ -3,6 +3,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // ----------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using Kerberos.NET; @@ -23,28 +24,32 @@ public class KdcHandlerTests : BaseTest private const string Realm = "CORP2.IDENTITYINTERVENTION.COM"; private const string Upn = "fake@" + Realm; + private const string Realm2 = "TEST.COM"; + private const string Upn2WithoutRealm = "fakeuser"; + // private const string Upn2WithoutRealm = "fakeuser@" + Realm2; + [TestMethod] public void KdcAsReqHandler_Sync() { - KrbAsRep asRep = RequestTgt(out _); + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out _, out KrbAsReq asReq); - Assert.IsNotNull(asRep); - Assert.AreEqual(Realm, asRep.CRealm); - Assert.AreEqual(Upn, asRep.CName.FullyQualifiedName); + ValidateAsRep(asRep, expectedCName: Upn, expectedCRealm: Realm, expectedSRealm: Realm, asReq); } [TestMethod] public void KdcTgsReqHandler_Sync() { - KrbAsRep asRep = RequestTgt(out KrbEncryptionKey tgtKey); + KrbAsRep asRep = RequestTgt(cname: Upn, crealm: Realm, srealm: Realm, out KrbEncryptionKey tgtKey); Assert.IsNotNull(asRep); + var spn = "host/foo." + Realm; + var tgsReq = KrbTgsReq.CreateTgsReq( new RequestServiceTicket { Realm = Realm, - ServicePrincipalName = "host/foo." + Realm + ServicePrincipalName = spn }, tgtKey, asRep, @@ -62,20 +67,238 @@ out KrbEncryptionKey sessionKey var tgsRep = KrbTgsRep.DecodeApplication(results); + var realmService = new FakeRealmService(Realm); + var ticketKey = realmService.Principals.Find(KrbPrincipalName.FromString(spn)).RetrieveLongTermCredential(); + + ValidateTgsRep( + tgsRep, + subSessionKey: sessionKey.AsKey(), + ticketKey: ticketKey, + expectedCName: Upn, + expectedCRealm: Realm, + expectedSName: spn, + expectedSRealm: Realm); + } + + [TestMethod] + public void KdcTgsReqHandler_Sync_ReferralTgt() + { + var sourceRealm = Realm2; + var destRealm = Realm; + var cname = new KrbPrincipalName + { + Type = PrincipalNameType.NT_PRINCIPAL, + Name = new[] { Upn2WithoutRealm } + }; + + KrbAsRep asRep = CreateReferralTgt(sourceRealm, destRealm, cname, out KerberosKey tgtKey, out KerberosKey asRepKey, out KrbEncryptionKey sessionKey); + + ValidateAsRep(asRep, expectedCName: Upn2WithoutRealm, expectedCRealm: sourceRealm, expectedSRealm: destRealm); + + // Send a TGS-REQ to get a service ticket in the destination realm + var spn = "host/foo." + Realm; + + var tgsReq = KrbTgsReq.CreateTgsReq( + new RequestServiceTicket + { + Realm = Realm, + ServicePrincipalName = spn + }, + sessionKey, + asRep, + out KrbEncryptionKey subSessionKey + ); + + var handler = new KdcTgsReqMessageHandler(tgsReq.EncodeApplication(), new KdcServerOptions + { + DefaultRealm = destRealm, + IsDebug = true, + RealmLocator = realm => new FakeRealmService(realm) + }); + + var results = handler.Execute(); + + var tgsRep = KrbTgsRep.DecodeApplication(results); + + var destRealmService = new FakeRealmService(destRealm); + var servicePrincipal = destRealmService.Principals.Find(KrbPrincipalName.FromString(spn), destRealm); + var ticketKey = servicePrincipal.RetrieveLongTermCredential(); + + ValidateTgsRep( + tgsRep, + subSessionKey: subSessionKey.AsKey(), + ticketKey: ticketKey, + expectedCName: Upn2WithoutRealm, + expectedCRealm: sourceRealm, + expectedSName: spn, + expectedSRealm: destRealm); + } + + private void ValidateAsRep(KrbAsRep asRep, string expectedCName, string expectedCRealm, string expectedSRealm, KrbAsReq asReq = null) + { + Assert.IsNotNull(asRep); + + // RFC 4120 Section 3.1.5 Receipt of KRB_AS_REP Message + // "If the reply message type is KRB_AS_REP, then the client verifies that the cname and crealm fields in + // the cleartext portion of the reply match what it requested." + Assert.AreEqual(MessageType.KRB_AS_REP, asRep.MessageType); + Assert.AreEqual(expectedCRealm, asRep.CRealm); + Assert.AreEqual(expectedCName, asRep.CName.FullyQualifiedName); + + // Optionally double check that the AS-REQ also matches + if (asReq != null) + { + Assert.AreEqual(Realm, asReq.Body.Realm); + Assert.AreEqual(Upn, asReq.Body.CName.FullyQualifiedName); + } + + // Check that correct TGT was generated + Assert.AreEqual(expectedSRealm, asRep.Ticket.Realm); + Assert.AreEqual($"krbtgt/{expectedSRealm}", asRep.Ticket.SName.FullyQualifiedName); + + // Clients can't decrypt TGTs usually, but for the sake of testing let's check what's inside + var realmService = new FakeRealmService(Realm); + var tgtPrincipalName = KrbPrincipalName.WellKnown.Krbtgt(Realm); + var tgtEncPartKey = realmService.Principals.Find(tgtPrincipalName).RetrieveLongTermCredential(); + + var ticketEncPart = asRep.Ticket.EncryptedPart.Decrypt( + tgtEncPartKey, + KeyUsage.Ticket, + d => KrbEncTicketPart.DecodeApplication(d) + ); + + Assert.IsNotNull(ticketEncPart); + Assert.AreEqual(expectedCRealm, ticketEncPart.CRealm); + Assert.AreEqual(expectedCName, ticketEncPart.CName.FullyQualifiedName); + } + + private void ValidateTgsRep(KrbTgsRep tgsRep, KerberosKey subSessionKey, KerberosKey ticketKey, string expectedCName, string expectedCRealm, string expectedSName, string expectedSRealm) + { Assert.IsNotNull(tgsRep); var encKdcRepPart = tgsRep.EncPart.Decrypt( - sessionKey.AsKey(), + subSessionKey, KeyUsage.EncTgsRepPartSubSessionKey, d => KrbEncTgsRepPart.DecodeApplication(d) ); Assert.IsNotNull(encKdcRepPart); + Assert.AreEqual(expectedCRealm, tgsRep.CRealm); + Assert.AreEqual(expectedCName, tgsRep.CName.FullyQualifiedName); + + Assert.IsNotNull(tgsRep.Ticket); + Assert.AreEqual(expectedSRealm, tgsRep.Ticket.Realm); + Assert.AreEqual(expectedSName, tgsRep.Ticket.SName.FullyQualifiedName); + + // Clients can't decrypt service tickets usually, but for the sake of testing let's check what's inside + var ticketEncPart = tgsRep.Ticket.EncryptedPart.Decrypt( + ticketKey, + KeyUsage.Ticket, + d => KrbEncTicketPart.DecodeApplication(d) + ); + + Assert.IsNotNull(ticketEncPart); + Assert.AreEqual(expectedCRealm, ticketEncPart.CRealm); + Assert.AreEqual(expectedCName, ticketEncPart.CName.FullyQualifiedName); + } + + private KrbAsRep CreateReferralTgt(string sourceRealm, string destRealm, KrbPrincipalName cname, out KerberosKey tgtKey, out KerberosKey asRepKey, out KrbEncryptionKey sessionKey) + { + var sourceRealmService = new FakeRealmService(sourceRealm); + + var sname = KrbPrincipalName.WellKnown.Krbtgt(destRealm); + var servicePrincipal = sourceRealmService.Principals.Find(sname, destRealm); + tgtKey = servicePrincipal.RetrieveLongTermCredential(); + + var clientPrincipal = sourceRealmService.Principals.Find(cname, sourceRealm); + asRepKey = clientPrincipal.RetrieveLongTermCredential(); + + sessionKey = KrbEncryptionKey.Generate(EncryptionType.AES128_CTS_HMAC_SHA256_128); + + DateTimeOffset now = DateTimeOffset.UtcNow; + + var encTicketPart = new KrbEncTicketPart() + { + CName = cname, + CRealm = sourceRealm, + Key = sessionKey, + AuthTime = now, + StartTime = now, + EndTime = now.AddHours(1), + RenewTill = now.AddDays(30), + Flags = TicketFlags.PreAuthenticated | TicketFlags.Initial | TicketFlags.Renewable | TicketFlags.Forwardable, + AuthorizationData = null, + CAddr = new KrbHostAddress[] { }, + Transited = new KrbTransitedEncoding() + }; + + KrbTicket ticket = new KrbTicket() + { + Realm = destRealm, + SName = sname, + EncryptedPart = KrbEncryptedData.Encrypt( + encTicketPart.EncodeApplication(), + tgtKey, + KeyUsage.Ticket + ) + }; + + KrbEncAsRepPart encAsRepPart = new KrbEncAsRepPart + { + AuthTime = encTicketPart.AuthTime, + StartTime = encTicketPart.AuthTime, + EndTime = encTicketPart.EndTime, + RenewTill = encTicketPart.RenewTill, + KeyExpiration = servicePrincipal.Expires, + Realm = destRealm, + SName = sname, + Flags = encTicketPart.Flags, + CAddr = encTicketPart.CAddr, + Key = sessionKey, + Nonce = 1234567890, + LastReq = new[] { new KrbLastReq { Type = 0, Value = now } }, + EncryptedPaData = new KrbMethodData + { + MethodData = new[] + { + new KrbPaData + { + Type = PaDataType.PA_SUPPORTED_ETYPES, + Value = servicePrincipal.SupportedEncryptionTypes.AsReadOnlyMemory(littleEndian: true) + } + } + } + }; + + KrbAsRep asRep = new KrbAsRep + { + CRealm = Realm2, + CName = new KrbPrincipalName + { + Type = PrincipalNameType.NT_PRINCIPAL, + Name = new[] { Upn2WithoutRealm } + }, + MessageType = MessageType.KRB_AS_REP, + Ticket = ticket, + EncPart = KrbEncryptedData.Encrypt( + encAsRepPart.EncodeApplication(), + asRepKey, + asRepKey.EncryptionType, + KeyUsage.EncAsRepPart + ) + }; + + return asRep; } - private static KrbAsRep RequestTgt(out KrbEncryptionKey sessionKey) + private KrbAsRep RequestTgt(string cname, string crealm, string srealm, out KrbEncryptionKey sessionKey) { - var cred = new KerberosPasswordCredential(Upn, "P@ssw0rd!") + return RequestTgt(cname, crealm, srealm, out sessionKey, out _); + } + + private KrbAsRep RequestTgt(string cname, string crealm, string srealm, out KrbEncryptionKey sessionKey, out KrbAsReq asReq) + { + var cred = new KerberosPasswordCredential(cname, "P@ssw0rd!", crealm) { // cheating by skipping the initial leg of requesting PA-type @@ -89,14 +312,14 @@ private static KrbAsRep RequestTgt(out KrbEncryptionKey sessionKey) Configuration = Krb5Config.Default() }; - var asReq = KrbAsReq.CreateAsReq( + asReq = KrbAsReq.CreateAsReq( cred, AuthenticationOptions.AllAuthentication ); var handler = new KdcAsReqMessageHandler(asReq.EncodeApplication(), new KdcServerOptions { - DefaultRealm = Realm, + DefaultRealm = srealm, IsDebug = true, RealmLocator = realm => new FakeRealmService(realm) }); diff --git a/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs b/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs index 9432e648..12659968 100644 --- a/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs +++ b/Tests/Tests.Kerberos.NET/Messages/KrbKdcRepTests.cs @@ -112,12 +112,15 @@ public void CreateServiceTicket() var tgsRep = KrbKdcRep.GenerateServiceTicket(new ServiceTicketRequest { - EncryptedPartKey = key, + ClientName = KrbPrincipalName.FromString("blah@test.com"), + ClientRealmName = "test.com", + Principal = new FakeKerberosPrincipal("blah@test.com"), + ServicePrincipal = new FakeKerberosPrincipal("blah@blah.com"), ServicePrincipalKey = key, - Principal = new FakeKerberosPrincipal("blah@blah2.com"), RealmName = "blah.com", - ClientRealmName = "test.com", + + EncryptedPartKey = key, Compatibility = KerberosCompatibilityFlags.IsolateRealmsConsistently, }); @@ -125,11 +128,11 @@ public void CreateServiceTicket() 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@blah2.com", tgsRep.CName.FullyQualifiedName); + 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@blah2.com", ticketEncPart.CName.FullyQualifiedName); + Assert.AreEqual("blah@test.com", ticketEncPart.CName.FullyQualifiedName); } [TestMethod] @@ -184,12 +187,14 @@ string expectedCRealm var tgsRep = KrbKdcRep.GenerateServiceTicket(new ServiceTicketRequest { + Principal = new FakeKerberosPrincipal($"blah@{crealm}"), + ClientName = KrbPrincipalName.FromString($"blah@{crealm}"), + ClientRealmName = crealm, + EncryptedPartKey = key, ServicePrincipal = new FakeKerberosPrincipal("blah@blah.com"), ServicePrincipalKey = key, - Principal = new FakeKerberosPrincipal("blah@blah2.com"), RealmName = realm, - ClientRealmName = crealm, Compatibility = compatibilityFlags, }); diff --git a/Tests/Tests.Kerberos.NET/Messages/KrbtgtTests.cs b/Tests/Tests.Kerberos.NET/Messages/KrbtgtTests.cs index fffa9305..4a19451e 100644 --- a/Tests/Tests.Kerberos.NET/Messages/KrbtgtTests.cs +++ b/Tests/Tests.Kerberos.NET/Messages/KrbtgtTests.cs @@ -141,6 +141,7 @@ string expectedRealm var principalKey = principal.RetrieveLongTermCredential(); +#pragma warning disable CS0618 // Type or member is obsolete var rst = new ServiceTicketRequest { SamAccountName = TestSamAccountName, @@ -149,6 +150,7 @@ string expectedRealm EncryptedPartKey = principalKey, ServicePrincipalKey = new KerberosKey(key: TgtKey, etype: EncryptionType.AES256_CTS_HMAC_SHA1_96) }; +#pragma warning restore CS0618 // Type or member is obsolete var tgt = KrbAsRep.GenerateTgt(rst, realmService);