-
Notifications
You must be signed in to change notification settings - Fork 104
Add GSS channel binding support per RFC 4121 section 4.1.1.2 for AP-REQ and TGS-REQ #422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| // ----------------------------------------------------------------------- | ||
| // Licensed to The .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| using System; | ||
| using System.Buffers.Binary; | ||
| using System.IO; | ||
| using System.Security.Cryptography; | ||
|
|
||
| namespace Kerberos.NET.Entities | ||
| { | ||
| /// <summary> | ||
| /// Represents gss_channel_bindings_struct per RFC 4121 section 4.1.1.2. | ||
| /// </summary> | ||
| public class GssChannelBindings | ||
| { | ||
| private const int SecChannelBindingsHeaderSize = 32; | ||
|
|
||
| public int InitiatorAddrType { get; set; } | ||
|
|
||
| public ReadOnlyMemory<byte> InitiatorAddress { get; set; } | ||
|
|
||
| public int AcceptorAddrType { get; set; } | ||
|
|
||
| public ReadOnlyMemory<byte> AcceptorAddress { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Protocol-specific channel binding data | ||
| /// e.g. tls-server-end-point or tls-unique as per RFC 5929 | ||
| /// </summary> | ||
| public ReadOnlyMemory<byte> ApplicationData { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Computes the 16-byte MD5 binding hash (Bnd field) per RFC 4121 section 4.1.1.2. | ||
| /// </summary> | ||
| public ReadOnlyMemory<byte> ComputeBindingHash() | ||
| { | ||
| using var stream = new MemoryStream(); | ||
| using var writer = new BinaryWriter(stream); | ||
|
|
||
| writer.Write(this.InitiatorAddrType); | ||
| writer.Write(this.InitiatorAddress.Length); | ||
|
|
||
| if (this.InitiatorAddress.Length > 0) | ||
| { | ||
| writer.Write(this.InitiatorAddress.ToArray()); | ||
| } | ||
|
|
||
| writer.Write(this.AcceptorAddrType); | ||
| writer.Write(this.AcceptorAddress.Length); | ||
|
|
||
| if (this.AcceptorAddress.Length > 0) | ||
| { | ||
| writer.Write(this.AcceptorAddress.ToArray()); | ||
| } | ||
|
|
||
| writer.Write(this.ApplicationData.Length); | ||
|
|
||
| if (this.ApplicationData.Length > 0) | ||
| { | ||
| writer.Write(this.ApplicationData.ToArray()); | ||
| } | ||
|
|
||
| var data = stream.ToArray(); | ||
|
|
||
| using var md5 = MD5.Create(); | ||
| return md5.ComputeHash(data); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Parses a raw SEC_CHANNEL_BINDINGS flat buffer (as returned by Windows SSPI) into a <see cref="GssChannelBindings"/>. | ||
| /// </summary> | ||
| public static GssChannelBindings FromSecChannelBindings(ReadOnlyMemory<byte> rawBuffer) | ||
| { | ||
| if (rawBuffer.Length < SecChannelBindingsHeaderSize) | ||
| { | ||
| throw new ArgumentException( | ||
| $"Buffer is too small to contain a SEC_CHANNEL_BINDINGS header. Expected at least {SecChannelBindingsHeaderSize} bytes.", | ||
| nameof(rawBuffer)); | ||
| } | ||
|
|
||
| var span = rawBuffer.Span; | ||
|
|
||
| var bindings = new GssChannelBindings | ||
| { | ||
| InitiatorAddrType = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(0)), | ||
| }; | ||
|
|
||
| int initiatorLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(4)); | ||
| int initiatorOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(8)); | ||
|
|
||
| bindings.AcceptorAddrType = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(12)); | ||
|
|
||
| int acceptorLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(16)); | ||
| int acceptorOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(20)); | ||
|
|
||
| int applicationDataLength = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(24)); | ||
| int applicationDataOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(28)); | ||
|
|
||
| if (initiatorLength > 0) | ||
| { | ||
| bindings.InitiatorAddress = rawBuffer.Slice(initiatorOffset, initiatorLength); | ||
| } | ||
|
|
||
| if (acceptorLength > 0) | ||
| { | ||
| bindings.AcceptorAddress = rawBuffer.Slice(acceptorOffset, acceptorLength); | ||
| } | ||
|
|
||
| if (applicationDataLength > 0) | ||
| { | ||
| bindings.ApplicationData = rawBuffer.Slice(applicationDataOffset, applicationDataLength); | ||
| } | ||
|
|
||
| return bindings; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
|
|
||
| using System; | ||
| using System.Globalization; | ||
| using System.Runtime.Versioning; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not needed? |
||
| using System.Security; | ||
| using System.Text; | ||
| using System.Threading.Tasks; | ||
|
|
@@ -46,6 +47,20 @@ public KerberosValidator(KeyTable keytab, ILoggerFactory logger = null, ITicketR | |
|
|
||
| public ValidationActions ValidateAfterDecrypt { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Expected channel bindings to validate during decryption. | ||
| /// </summary> | ||
| public GssChannelBindings ExpectedChannelBindings { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Property that accepts a raw SEC_CHANNEL_BINDINGS buffer (as returned by Windows SSPI) | ||
| /// and converts it to <see cref="ExpectedChannelBindings"/>. | ||
| /// </summary> | ||
| public ReadOnlyMemory<byte> ExpectedRawChannelBindings | ||
| { | ||
| set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); } | ||
| } | ||
|
|
||
| private Func<DateTimeOffset> nowFunc; | ||
|
|
||
| public Func<DateTimeOffset> Now | ||
|
|
@@ -83,6 +98,7 @@ public async Task<DecryptedKrbApReq> Validate(ReadOnlyMemory<byte> requestBytes) | |
| this.logger.LogTrace("Kerberos request decrypted {SName}", decryptedToken.SName.FullyQualifiedName); | ||
|
|
||
| decryptedToken.Now = this.Now; | ||
| decryptedToken.ExpectedChannelBindings = this.ExpectedChannelBindings; | ||
|
|
||
| if (this.ValidateAfterDecrypt > 0) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,20 @@ public abstract class KdcMessageHandlerBase | |
|
|
||
| protected KdcServerOptions Options { get; } | ||
|
|
||
| /// <summary> | ||
| /// Expected channel bindings for this request's TGS-REQ validation. | ||
| /// </summary> | ||
| public GssChannelBindings ExpectedChannelBindings { get; set; } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe makes sense to just store the hash, instead of the full object? Seems like you just need the hash |
||
|
|
||
| /// <summary> | ||
| /// Accepts a raw SEC_CHANNEL_BINDINGS buffer | ||
| /// and converts it to <see cref="ExpectedChannelBindings"/>. | ||
| /// </summary> | ||
| public ReadOnlyMemory<byte> ExpectedRawChannelBindings | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I feel like intention might be clearer with a setter method instead of a setter-only field. Or you could just make the caller call GssChannelBindings.FromSecChannelBindings. But no strong opinion on this. |
||
| { | ||
| set { this.ExpectedChannelBindings = GssChannelBindings.FromSecChannelBindings(value); } | ||
| } | ||
|
|
||
| protected IRealmService RealmService { get; private set; } | ||
|
|
||
| public IDictionary<PaDataType, PreAuthHandlerConstructor> PreAuthHandlers => this.preAuthHandlers; | ||
|
|
@@ -103,7 +117,10 @@ public virtual async Task<ReadOnlyMemory<byte>> ExecuteAsync() | |
| { | ||
| try | ||
| { | ||
| var context = new PreAuthenticationContext(); | ||
| var context = new PreAuthenticationContext | ||
| { | ||
| ExpectedChannelBindings = this.ExpectedChannelBindings | ||
| }; | ||
|
|
||
| this.DecodeMessage(context); | ||
|
|
||
|
|
@@ -127,7 +144,10 @@ public virtual ReadOnlyMemory<byte> Execute() | |
| { | ||
| try | ||
| { | ||
| var context = new PreAuthenticationContext(); | ||
| var context = new PreAuthenticationContext | ||
| { | ||
| ExpectedChannelBindings = this.ExpectedChannelBindings | ||
| }; | ||
|
|
||
| this.DecodeMessage(context); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you passed ValidationActions.ChannelBinding but forgot to pass ExpectedChannelBindings -- should this be an exception? Or maybe it's not a wise idea -- but just trying to see if there's a way to not fail silently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe you could make ExpectedChannelBindings a mandatory param of Validate?