Skip to content

Commit 9422d22

Browse files
authored
Merge pull request #407 from MaximeKjaer/cname-copy
Ensure cname precisely matches what was in request
2 parents da52236 + cda9b28 commit 9422d22

12 files changed

Lines changed: 378 additions & 49 deletions

Kerberos.NET/Entities/Krb/KrbKdcRep.cs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,6 @@ private static KrbEncTicketPart CreateEncTicketPart(
194194
KrbEncryptionKey sessionKey
195195
)
196196
{
197-
var cname = CreateCNameForTicket(request);
198-
199197
var flags = request.Flags;
200198

201199
if (request.PreAuthenticationData?.Any(r => r.Type == PaDataType.PA_REQ_ENC_PA_REP) ?? false)
@@ -209,7 +207,7 @@ KrbEncryptionKey sessionKey
209207

210208
var encTicketPart = new KrbEncTicketPart()
211209
{
212-
CName = cname,
210+
CName = CreateCNameForTicket(request),
213211
CRealm = request.Compatibility.HasFlag(KerberosCompatibilityFlags.IsolateRealmsConsistently) ? request.ClientRealmName : request.RealmName,
214212
Key = sessionKey,
215213
AuthTime = request.Now,
@@ -233,21 +231,44 @@ KrbEncryptionKey sessionKey
233231

234232
private static KrbPrincipalName CreateCNameForTicket(ServiceTicketRequest request)
235233
{
236-
if (string.IsNullOrEmpty(request.SamAccountName))
234+
// If ClientName is explicitly set, use that. This is the preferred method for the caller to indicate what
235+
// cname should be used.
236+
if (request.ClientName != null)
237237
{
238-
return KrbPrincipalName.FromPrincipal(
239-
request.Principal,
240-
realm: request.Compatibility.HasFlag(KerberosCompatibilityFlags.IsolateRealmsConsistently) ?
241-
request.ClientRealmName :
242-
request.RealmName
243-
);
238+
return request.ClientName;
244239
}
245240

246-
return new KrbPrincipalName
241+
// Otherwise, if SamAccountName is set, use that as the principal name.
242+
// This is not the recommended method, but is supported for backwards compatibility.
243+
#pragma warning disable CS0618 // Type or member is obsolete
244+
if (!string.IsNullOrEmpty(request.SamAccountName))
247245
{
248-
Type = PrincipalNameType.NT_PRINCIPAL,
249-
Name = new[] { request.SamAccountName }
250-
};
246+
// Note that the name is returned in a single part here, even if the request may have had multiple parts.
247+
// This may be okay for AS-REQs with Canonicalize set, but it is not spec-compliant for other scenarios.
248+
return new KrbPrincipalName
249+
{
250+
Type = PrincipalNameType.NT_PRINCIPAL,
251+
Name = new[] { request.SamAccountName }
252+
};
253+
}
254+
#pragma warning restore CS0618 // Type or member is obsolete
255+
256+
// Lastly, if neither are set, derive from the principal.
257+
//
258+
// Note: this might not be correct in all scenarios. For instance, if the client does not accept
259+
// name canonicalization (i.e., the Canonicalize flag is not set), then it's not spec-compliant to deviate
260+
// from the requested cname. Also, in TGS-REP, the cname should match what's in the TGT, and should not be
261+
// derived from the found principal. It is the responsibility of the caller to decide whether the request
262+
// warrants passing Principal only, or forcing a specific cname via ClientName.
263+
//
264+
// Note: historically Kerberos.NET had a bug where the service realm was used to derive cname from the principal.
265+
// However, it should be the client realm. This has been corrected under the IsolateRealmsConsistently flag.
266+
return KrbPrincipalName.FromPrincipal(
267+
request.Principal,
268+
realm: request.Compatibility.HasFlag(KerberosCompatibilityFlags.IsolateRealmsConsistently) ?
269+
request.ClientRealmName :
270+
request.RealmName
271+
);
251272
}
252273

253274
private static IEnumerable<KrbAuthorizationData> GenerateAuthorizationData(ServiceTicketRequest request)

Kerberos.NET/Entities/Krb/ServiceTicketRequest.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ public struct ServiceTicketRequest : IEquatable<ServiceTicketRequest>
2828
public KerberosKey KdcAuthorizationKey { get; set; }
2929

3030
/// <summary>
31-
/// The realm name for which the requested identity originated
31+
/// The client name (cname) of the identity requesting the ticket.
32+
/// If this is not set, <see cref="SamAccountName"/> or <see cref="Principal"/> will be used.
33+
/// </summary>
34+
public KrbPrincipalName ClientName { get; set; }
35+
36+
/// <summary>
37+
/// The client realm (crealm) name of the identity requesting the ticket
3238
/// </summary>
3339
public string ClientRealmName { get; set; }
3440

@@ -110,9 +116,12 @@ public struct ServiceTicketRequest : IEquatable<ServiceTicketRequest>
110116

111117
/// <summary>
112118
/// SAM account name to be used to generate TGT for Windows specific user principal.
113-
/// If this parameter contains valid string (not empty), CName of encrypted part of ticket
114-
/// will be created based on provided SamAccountName.
119+
/// This is only used if (1) <see cref="ClientName"/> is not set, and (2) it is a valid (not empty) string.
120+
/// Used to compute the cname of a KDC-REP.
115121
/// </summary>
122+
[Obsolete(
123+
"Using SamAccountName may cause non spec-compliant behavior. Use ClientName instead to set the client " +
124+
"principal name that should be used in Kerberos responses in a spec-compliant manner.")]
116125
public string SamAccountName { get; set; }
117126

118127
/// <summary>
@@ -179,6 +188,21 @@ public bool Equals(ServiceTicketRequest other)
179188
return false;
180189
}
181190

191+
if (other.ClientName != this.ClientName)
192+
{
193+
return false;
194+
}
195+
196+
if (other.ClientRealmName != this.ClientRealmName)
197+
{
198+
return false;
199+
}
200+
201+
if (other.EncryptedPartEType != this.EncryptedPartEType)
202+
{
203+
return false;
204+
}
205+
182206
if (other.EncryptedPartKey != this.EncryptedPartKey)
183207
{
184208
return false;
@@ -249,10 +273,12 @@ public bool Equals(ServiceTicketRequest other)
249273
return false;
250274
}
251275

276+
#pragma warning disable CS0618 // Type or member is obsolete
252277
if (other.SamAccountName != this.SamAccountName)
253278
{
254279
return false;
255280
}
281+
#pragma warning restore CS0618 // Type or member is obsolete
256282

257283
if (other.ServicePrincipal != this.ServicePrincipal)
258284
{
@@ -279,8 +305,13 @@ public bool Equals(ServiceTicketRequest other)
279305

280306
public override int GetHashCode()
281307
{
308+
#pragma warning disable CS0618 // Type or member is obsolete
282309
return EntityHashCode.GetHashCode(
283310
this.Addresses,
311+
this.ClientName,
312+
this.ClientRealmName,
313+
this.Compatibility,
314+
this.EncryptedPartEType,
284315
this.EncryptedPartKey,
285316
this.EndTime,
286317
this.Flags,
@@ -301,6 +332,7 @@ public override int GetHashCode()
301332
this.StartTime,
302333
this.Compatibility
303334
);
335+
#pragma warning restore CS0618 // Type or member is obsolete
304336
}
305337

306338
public static bool operator ==(ServiceTicketRequest left, ServiceTicketRequest right)

Kerberos.NET/Server/KdcAsReqMessageHandler.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ private ReadOnlyMemory<byte> GenerateAsRep(KrbAsReq asReq, PreAuthenticationCont
167167

168168
var rst = new ServiceTicketRequest
169169
{
170+
ClientRealmName = asReq.Body.Realm,
170171
Principal = context.Principal,
171172
EncryptedPartKey = context.EncryptedPartKey,
172173
EncryptedPartEType = context.EncryptedPartEType,
@@ -193,10 +194,30 @@ private ReadOnlyMemory<byte> GenerateAsRep(KrbAsReq asReq, PreAuthenticationCont
193194

194195
// Canonicalize means the CName in the reply is allowed to be different from the CName in the request.
195196
// If this is not allowed, then we must use the CName from the request. Otherwise, we will set the CName
196-
// to what we have in our realm, i.e. user@realm.
197+
// to what we have in our realm, i.e. user@realm (which will be inferred from the Principal set above).
198+
//
199+
// RFC 4120 section 3.1.5. Receipt of KRB_AS_REP Message
200+
// -----------------------------------------------------
201+
// If the reply message type is KRB_AS_REP, then the client verifies that the cname and crealm fields in
202+
// the cleartext portion of the reply match what it requested.
203+
//
204+
// RFC 6806 section 6. Name Canonicalization
205+
// -----------------------------------------
206+
// If the "canonicalize" KDC option is set, then the KDC MAY change the client and server principal names
207+
// and types in the AS response and ticket returned from those in the request.
197208
if (!asReq.Body.KdcOptions.HasFlag(KdcOptions.Canonicalize))
198209
{
199-
rst.SamAccountName = asReq.Body.CName.FullyQualifiedName;
210+
if (this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.EnableSpecCompliantCNameHandling))
211+
{
212+
rst.ClientName = asReq.Body.CName;
213+
}
214+
else
215+
{
216+
#pragma warning disable CS0618 // Type or member is obsolete
217+
rst.SamAccountName = asReq.Body.CName.FullyQualifiedName;
218+
#pragma warning restore CS0618 // Type or member is obsolete
219+
}
220+
200221
}
201222

202223
if (rst.EncryptedPartKey == null)

Kerberos.NET/Server/KdcTgsReqMessageHandler.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,13 @@ public override ReadOnlyMemory<byte> ExecuteCore(PreAuthenticationContext contex
265265

266266
var rst = new ServiceTicketRequest
267267
{
268+
ClientRealmName = context.Ticket.CRealm,
268269
KdcAuthorizationKey = context.EvidenceTicketKey,
269270
Principal = context.Principal,
270271
EncryptedPartKey = context.EncryptedPartKey,
271272
EncryptedPartEType = context.EncryptedPartEType,
272273
ServicePrincipal = context.ServicePrincipal,
273274
ServicePrincipalKey = serviceKey,
274-
ClientRealmName = context.ClientRealm,
275275
RealmName = tgsReq.Body.Realm,
276276
Addresses = tgsReq.Body.Addresses,
277277
RenewTill = context.Ticket.RenewTill,
@@ -290,13 +290,35 @@ public override ReadOnlyMemory<byte> ExecuteCore(PreAuthenticationContext contex
290290
Compatibility = this.RealmService.Settings.Compatibility,
291291
};
292292

293-
// this introduced an annoying regression in a separate party and this is a workaround to make sure it
294-
// uses the original behavior in cases where that's expected
295-
296-
if (!this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.DoNotCanonicalizeTgsReqFromTgt) &&
293+
if (this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.EnableSpecCompliantCNameHandling))
294+
{
295+
// In TGS-REP, we should always reply with cname/crealm copied from the TGT, even when the Canonicalize
296+
// flag is set in TGS-REQ.
297+
//
298+
// RFC 4120 § 3.3.3 Generation of KRB_TGS_REP Message
299+
// --------------------------------------------------
300+
// "By default, the address field, the client's name and realm, the list of transited realms, the time
301+
// of initial authentication, the expiration time, and the authorization data of the newly-issued
302+
// ticket will be copied from the TGT or renewable ticket."
303+
//
304+
// RFC 6806 § 6. Name Canonicalization
305+
// -----------------------------------
306+
// "If the "canonicalize" KDC option is set, then the KDC MAY change the client and server principal
307+
// names and types in the AS response and ticket returned from those in the request. Names MUST NOT be
308+
// changed in the response to a TGS request, although it is common for KDCs to maintain a set of
309+
// aliases for service principals."
310+
rst.ClientName = context.Ticket.CName;
311+
}
312+
else if (!this.RealmService.Settings.Compatibility.HasFlag(KerberosCompatibilityFlags.DoNotCanonicalizeTgsReqFromTgt) &&
297313
tgsReq.Body.KdcOptions.HasFlag(KdcOptions.Canonicalize))
298314
{
315+
// The code below introduced an annoying regression in a separate party.
316+
// The compatibility flag is a workaround to make sure it can use the original behavior in cases where
317+
// that's expected.
318+
319+
#pragma warning disable CS0612 // Type or member is obsolete
299320
rst.SamAccountName = context.GetState<TgsState>(PaDataType.PA_TGS_REQ).DecryptedApReq.Ticket.CName.FullyQualifiedName;
321+
#pragma warning restore CS0612 // Type or member is obsolete
300322
}
301323

302324
// this is set here instead of in GenerateServiceTicket because GST is used by unit tests to

Kerberos.NET/Server/KerberosCompatibilityFlags.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,12 @@ public enum KerberosCompatibilityFlags
3333
/// fields or properties. This separates the names into two.
3434
/// </summary>
3535
IsolateRealmsConsistently = 1 << 2,
36+
37+
/// <summary>
38+
/// CName handling was historically non-spec compliant in some cases.
39+
/// This flag enables handling that more strictly adheres to the spec, for better compliance
40+
/// with other implementations.
41+
/// </summary>
42+
EnableSpecCompliantCNameHandling = 1 << 3,
3643
}
3744
}

Kerberos.NET/Server/PaDataTgsTicketHandler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ public override KrbPaData Validate(KrbKdcReq asReq, PreAuthenticationContext con
105105

106106
context.EncryptedPartKey = state.DecryptedApReq.SessionKey;
107107
context.Ticket = state.DecryptedApReq.Ticket;
108-
context.ClientRealm = state.DecryptedApReq.Ticket.CRealm;
109108

110109
return null;
111110
}

Kerberos.NET/Server/PreAuthenticationContext.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ public class PreAuthenticationContext
3232
/// </summary>
3333
public KerberosKey EvidenceTicketKey { get; set; }
3434

35-
/// <summary>
36-
/// The name of the realm that the client issued a TGT from.
37-
/// </summary>
38-
public string ClientRealm { get; set; }
39-
4035
/// <summary>
4136
/// The identity that will be the subject of the issued ticket.
4237
/// </summary>

Tests/Tests.Kerberos.NET/Client/HttpKerberosTransportTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
9292

9393
var rst = new ServiceTicketRequest
9494
{
95+
ClientName = KrbPrincipalName.FromPrincipal(principal),
96+
ClientRealmName = Realm,
9597
Principal = principal,
9698
EncryptedPartKey = principalKey,
9799
ServicePrincipalKey = new KerberosKey(key: TgtKey, etype: EncryptionType.AES256_CTS_HMAC_SHA1_96)

Tests/Tests.Kerberos.NET/FakeRealmService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class FakeRealmService : IRealmService
1313
{
1414
private readonly KerberosCompatibilityFlags compatibilityFlags;
1515

16-
public FakeRealmService(string realm, Krb5Config config = null, KerberosCompatibilityFlags compatibilityFlags = KerberosCompatibilityFlags.IsolateRealmsConsistently)
16+
public FakeRealmService(string realm, Krb5Config config = null, KerberosCompatibilityFlags compatibilityFlags = KerberosCompatibilityFlags.IsolateRealmsConsistently | KerberosCompatibilityFlags.EnableSpecCompliantCNameHandling)
1717
{
1818
this.Name = realm;
1919
this.Configuration = config ?? Krb5Config.Kdc();

0 commit comments

Comments
 (0)