Skip to content

Commit f41e7df

Browse files
authored
Merge pull request #422 from thadumi/cbt-validation
Add GSS channel binding support per RFC 4121 section 4.1.1.2 for AP-REQ and TGS-REQ
2 parents dda497e + 01c3908 commit f41e7df

File tree

9 files changed

+734
-10
lines changed

9 files changed

+734
-10
lines changed

Kerberos.NET/Crypto/DecryptedKrbApReq.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ public DecryptedKrbApReq(KrbApReq token, MessageType incomingMessageType = Messa
3636

3737
public KrbEncKrbCredPart DelegationTicket { get; private set; }
3838

39+
/// <summary>
40+
/// The channel binding hash (Bnd field) extracted from the authenticator checksum,
41+
/// as described in RFC 4121 section 4.1.1.2. This is a 16-byte MD5 hash of the
42+
/// <see cref="GssChannelBindings"/> structure. Will be null or empty if no channel
43+
/// bindings were supplied by the initiator.
44+
/// </summary>
45+
public ReadOnlyMemory<byte> ChannelBindingHash { get; private set; }
46+
47+
/// <summary>
48+
/// Expected channel bindings to validate against when <see cref="ValidationActions.ChannelBinding"/> is enabled.
49+
/// </summary>
50+
public GssChannelBindings ExpectedChannelBindings { get; set; }
51+
52+
/// <summary>
53+
/// Accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI)
54+
/// and converts it to <see cref="ExpectedChannelBindings"/>.
55+
/// </summary>
56+
public void SetExpectedChannelBindingsFromSecChannelBindings(ReadOnlyMemory<byte> buffer)
57+
{
58+
this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(buffer);
59+
}
60+
3961
public KerberosKey SessionKey { get; private set; }
4062

4163
private readonly KrbApReq token;
@@ -161,6 +183,11 @@ private KrbEncKrbCredPart TryExtractDelegationTicket(KrbChecksum checksum)
161183

162184
var delegationInfo = checksum.DecodeDelegation();
163185

186+
if (delegationInfo != null)
187+
{
188+
this.ChannelBindingHash = delegationInfo.ChannelBindingHash;
189+
}
190+
164191
var delegation = delegationInfo?.DelegationTicket;
165192

166193
if (delegation == null)
@@ -212,6 +239,35 @@ public override void Validate(ValidationActions validation)
212239
{
213240
this.ValidateTicketRenewal(this.Ticket.RenewTill, now, this.Skew);
214241
}
242+
243+
if (validation.HasFlag(ValidationActions.ChannelBinding))
244+
{
245+
this.ValidateChannelBinding();
246+
}
247+
}
248+
249+
protected virtual void ValidateChannelBinding()
250+
{
251+
if (this.ExpectedChannelBindings == null)
252+
{
253+
return;
254+
}
255+
256+
var expectedHash = this.ExpectedChannelBindings.ComputeBindingHash();
257+
258+
if (this.ChannelBindingHash.Length == 0)
259+
{
260+
throw new KerberosValidationException(
261+
"Channel Bindings are required by the acceptor but were not supplied by the initiator."
262+
);
263+
}
264+
265+
if (!KerberosCryptoTransformer.AreEqualSlow(expectedHash.Span, this.ChannelBindingHash.Span))
266+
{
267+
throw new KerberosValidationException(
268+
"The Channel Bindings hash from the initiator does not match the expected channel bindings."
269+
);
270+
}
215271
}
216272

217273
public override string ToString()
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// -----------------------------------------------------------------------
2+
// Licensed to The .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// -----------------------------------------------------------------------
5+
6+
using System;
7+
using System.Buffers.Binary;
8+
using System.IO;
9+
using System.Security.Cryptography;
10+
11+
namespace Kerberos.NET.Entities
12+
{
13+
/// <summary>
14+
/// Represents gss_channel_bindings_struct per RFC 4121 section 4.1.1.2.
15+
/// </summary>
16+
public class GssChannelBindings
17+
{
18+
private const int SecChannelBindingsHeaderSize = 32;
19+
20+
public int InitiatorAddrType { get; set; }
21+
22+
public ReadOnlyMemory<byte> InitiatorAddress { get; set; }
23+
24+
public int AcceptorAddrType { get; set; }
25+
26+
public ReadOnlyMemory<byte> AcceptorAddress { get; set; }
27+
28+
/// <summary>
29+
/// Protocol-specific channel binding data
30+
/// e.g. tls-server-end-point or tls-unique as per RFC 5929
31+
/// </summary>
32+
public ReadOnlyMemory<byte> ApplicationData { get; set; }
33+
34+
/// <summary>
35+
/// Computes the 16-byte MD5 binding hash (Bnd field) per RFC 4121 section 4.1.1.2.
36+
/// </summary>
37+
public ReadOnlyMemory<byte> ComputeBindingHash()
38+
{
39+
using var stream = new MemoryStream();
40+
using var writer = new BinaryWriter(stream);
41+
42+
writer.Write(this.InitiatorAddrType);
43+
writer.Write(this.InitiatorAddress.Length);
44+
45+
if (this.InitiatorAddress.Length > 0)
46+
{
47+
writer.Write(this.InitiatorAddress.ToArray());
48+
}
49+
50+
writer.Write(this.AcceptorAddrType);
51+
writer.Write(this.AcceptorAddress.Length);
52+
53+
if (this.AcceptorAddress.Length > 0)
54+
{
55+
writer.Write(this.AcceptorAddress.ToArray());
56+
}
57+
58+
writer.Write(this.ApplicationData.Length);
59+
60+
if (this.ApplicationData.Length > 0)
61+
{
62+
writer.Write(this.ApplicationData.ToArray());
63+
}
64+
65+
var data = stream.ToArray();
66+
67+
using var md5 = MD5.Create();
68+
return md5.ComputeHash(data);
69+
}
70+
71+
/// <summary>
72+
/// Parses a raw SEC_CHANNEL_BINDINGS flat buffer (as returned by Windows SSPI) into a <see cref="GssChannelBindings"/>.
73+
/// </summary>
74+
public static GssChannelBindings FromSecChannelBindings(ReadOnlyMemory<byte> rawBuffer)
75+
{
76+
if (rawBuffer.Length < SecChannelBindingsHeaderSize)
77+
{
78+
throw new ArgumentException(
79+
$"Buffer is too small to contain a SEC_CHANNEL_BINDINGS header. Expected at least {SecChannelBindingsHeaderSize} bytes.",
80+
nameof(rawBuffer));
81+
}
82+
83+
var span = rawBuffer.Span;
84+
85+
var bindings = new GssChannelBindings
86+
{
87+
InitiatorAddrType = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(0)),
88+
};
89+
90+
int initiatorLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(4));
91+
int initiatorOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(8));
92+
93+
bindings.AcceptorAddrType = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(12));
94+
95+
int acceptorLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(16));
96+
int acceptorOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(20));
97+
98+
int applicationDataLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(24));
99+
int applicationDataOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(28));
100+
101+
if (initiatorLength > 0)
102+
{
103+
bindings.InitiatorAddress = rawBuffer.Slice(initiatorOffset, initiatorLength);
104+
}
105+
106+
if (acceptorLength > 0)
107+
{
108+
bindings.AcceptorAddress = rawBuffer.Slice(acceptorOffset, acceptorLength);
109+
}
110+
111+
if (applicationDataLength > 0)
112+
{
113+
bindings.ApplicationData = rawBuffer.Slice(applicationDataOffset, applicationDataLength);
114+
}
115+
116+
return bindings;
117+
}
118+
}
119+
}

Kerberos.NET/Entities/Krb/DelegationInfo.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,25 @@ public DelegationInfo()
4242
public DelegationInfo(RequestServiceTicket rst)
4343
{
4444
this.Flags = rst.GssContextFlags;
45+
46+
if (rst.ChannelBindings != null)
47+
{
48+
this.ChannelBindingHash = rst.ChannelBindings.ComputeBindingHash();
49+
}
4550
}
4651

4752
public ReadOnlyMemory<byte> Encode()
4853
{
4954
using (var stream = new MemoryStream())
5055
using (var writer = new BinaryWriter(stream))
5156
{
52-
if (this.ChannelBinding.Length == 0)
57+
if (this.ChannelBindingHash.Length == 0)
5358
{
54-
this.ChannelBinding = new byte[ChannelBindingLength];
59+
this.ChannelBindingHash = new byte[ChannelBindingLength];
5560
}
5661

57-
writer.Write(this.ChannelBinding.Length);
58-
writer.Write(this.ChannelBinding.ToArray());
62+
writer.Write(this.ChannelBindingHash.Length);
63+
writer.Write(this.ChannelBindingHash.ToArray());
5964

6065
if (this.DelegationTicket != null)
6166
{
@@ -85,7 +90,7 @@ public DelegationInfo Decode(ReadOnlyMemory<byte> value)
8590
{
8691
this.Length = reader.ReadInt32();
8792

88-
this.ChannelBinding = reader.ReadBytes(this.Length);
93+
this.ChannelBindingHash = reader.ReadBytes(this.Length);
8994

9095
this.Flags = (GssContextEstablishmentFlag)reader.ReadBytes(4).AsLong(littleEndian: true);
9196

@@ -124,7 +129,7 @@ public DelegationInfo Decode(ReadOnlyMemory<byte> value)
124129

125130
public int Length { get; set; }
126131

127-
public ReadOnlyMemory<byte> ChannelBinding { get; set; }
132+
public ReadOnlyMemory<byte> ChannelBindingHash { get; set; }
128133

129134
public GssContextEstablishmentFlag Flags { get; set; }
130135

@@ -134,4 +139,4 @@ public DelegationInfo Decode(ReadOnlyMemory<byte> value)
134139

135140
public ReadOnlyMemory<byte> Extensions { get; set; }
136141
}
137-
}
142+
}

Kerberos.NET/Entities/Krb/KrbTgsReq.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ out KrbEncryptionKey sessionKey
113113
KeyUsage.PaTgsReqChecksum
114114
);
115115

116-
var tgtApReq = CreateApReq(kdcRep, tgtSessionKey, bodyChecksum, out sessionKey);
116+
var tgtApReq = CreateApReq(kdcRep, tgtSessionKey, bodyChecksum, rst.ChannelBindings, out sessionKey);
117117

118118
var pacOptions = new KrbPaPacOptions
119119
{
@@ -200,7 +200,7 @@ private static ReadOnlyMemory<byte> EncodeS4URequest(string s4u, X509Certificate
200200
return paX509.Encode();
201201
}
202202

203-
private static KrbApReq CreateApReq(KrbKdcRep kdcRep, KrbEncryptionKey tgtSessionKey, KrbChecksum checksum, out KrbEncryptionKey sessionKey)
203+
private static KrbApReq CreateApReq(KrbKdcRep kdcRep, KrbEncryptionKey tgtSessionKey, KrbChecksum checksum, GssChannelBindings channelBindings, out KrbEncryptionKey sessionKey)
204204
{
205205
var tgt = kdcRep.Ticket;
206206

@@ -212,6 +212,13 @@ private static KrbApReq CreateApReq(KrbKdcRep kdcRep, KrbEncryptionKey tgtSessio
212212
Checksum = checksum
213213
};
214214

215+
if (channelBindings != null)
216+
{
217+
var delegInfo = new DelegationInfo();
218+
delegInfo.ChannelBindingHash = channelBindings.ComputeBindingHash();
219+
authenticator.Checksum = KrbChecksum.EncodeDelegationChecksum(delegInfo);
220+
}
221+
215222
sessionKey = KrbEncryptionKey.Generate(tgtSessionKey.EType);
216223

217224
sessionKey.Usage = KeyUsage.EncTgsRepPartSubSessionKey;

Kerberos.NET/Entities/RequestServiceTicket.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public struct RequestServiceTicket : IEquatable<RequestServiceTicket>
6565
/// </summary>
6666
public GssContextEstablishmentFlag GssContextFlags { get; set; }
6767

68+
/// <summary>
69+
/// Optional GSS channel bindings to include in the authenticator checksum.
70+
/// </summary>
71+
public GssChannelBindings ChannelBindings { get; set; }
72+
6873
/// <summary>
6974
/// Includes additional configuration details for the request.
7075
/// </summary>

Kerberos.NET/KerberosValidator.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System;
77
using System.Globalization;
8+
using System.Runtime.Versioning;
89
using System.Security;
910
using System.Text;
1011
using System.Threading.Tasks;
@@ -46,6 +47,20 @@ public KerberosValidator(KeyTable keytab, ILoggerFactory logger = null, ITicketR
4647

4748
public ValidationActions ValidateAfterDecrypt { get; set; }
4849

50+
/// <summary>
51+
/// Expected channel bindings to validate during decryption.
52+
/// </summary>
53+
public GssChannelBindings ExpectedChannelBindings { get; set; }
54+
55+
/// <summary>
56+
/// Accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI)
57+
/// and converts it to <see cref="ExpectedChannelBindings"/>.
58+
/// </summary>
59+
public void SetExpectedChannelBindingsFromSecChannelBindings(ReadOnlyMemory<byte> buffer)
60+
{
61+
this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(buffer);
62+
}
63+
4964
private Func<DateTimeOffset> nowFunc;
5065

5166
public Func<DateTimeOffset> Now
@@ -83,6 +98,7 @@ public async Task<DecryptedKrbApReq> Validate(ReadOnlyMemory<byte> requestBytes)
8398
this.logger.LogTrace("Kerberos request decrypted {SName}", decryptedToken.SName.FullyQualifiedName);
8499

85100
decryptedToken.Now = this.Now;
101+
decryptedToken.ExpectedChannelBindings = this.ExpectedChannelBindings;
86102

87103
if (this.ValidateAfterDecrypt > 0)
88104
{

Kerberos.NET/Server/PaDataTgsTicketHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public PaDataTgsTicketHandler(IRealmService service)
1717
{
1818
}
1919

20-
public ValidationActions Validation { get; set; } = ValidationActions.All & ~ValidationActions.Replay;
20+
public ValidationActions Validation { get; set; } = ValidationActions.All & ~ValidationActions.Replay & ~ValidationActions.ChannelBinding;
2121

2222
/// <summary>
2323
/// Executes before the validation stage and can be used for initial decoding of the message.

Kerberos.NET/ValidationAction.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public enum ValidationActions
7070
/// </summary>
7171
SequenceNumberGreaterThan = 1 << 9,
7272

73+
/// <summary>
74+
/// Validates channel bindings in the authenticator checksum.
75+
/// </summary>
76+
ChannelBinding = 1 << 10,
77+
7378
/// <summary>
7479
/// Indicates all validation actions must be invoked.
7580
/// </summary>

0 commit comments

Comments
 (0)