Skip to content

Commit

Permalink
Support Extended Protection for Authentication (aka Channel binding)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwrock committed Feb 16, 2016
1 parent cbf8601 commit 1af7775
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 9 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ language: ruby
rvm:
- 1.9.3
- 1.9.2
- 1.8.7
- 2.0.0
- rbx-19mode
- rbx-18mode
Expand Down
3 changes: 3 additions & 0 deletions lib/net/ntlm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
require 'socket'

# Load Order is important here
require 'net/ntlm/exceptions'
require 'net/ntlm/field'
require 'net/ntlm/int16_le'
require 'net/ntlm/int32_le'
Expand All @@ -60,6 +61,8 @@
require 'net/ntlm/encode_util'

require 'net/ntlm/client'
require 'net/ntlm/channel_binding'
require 'net/ntlm/target_info'

module Net
module NTLM
Expand Down
65 changes: 65 additions & 0 deletions lib/net/ntlm/channel_binding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module Net
module NTLM
class ChannelBinding

# Creates a ChannelBinding used for Extended Protection Authentication
# @see http://blogs.msdn.com/b/openspecification/archive/2013/03/26/ntlm-and-channel-binding-hash-aka-exteneded-protection-for-authentication.aspx
#
# @param outer_channel [OpenSSL::X509::Certificate] Server certificate securing
# the outer TLS channel
# @return [NTLM::ChannelBinding] A ChannelBinding holding a token that can be
# embedded in a {Type3} message
def self.create(outer_channel)
new(outer_channel)
end

# @param outer_channel [OpenSSL::X509::Certificate] Server certificate securing
# the outer TLS channel
def initialize(outer_channel)
@channel = outer_channel
@unique_prefix = 'tls-server-end-point'
@initiator_addtype = 0
@initiator_address_length = 0
@acceptor_addrtype = 0
@acceptor_address_length = 0
end

attr_reader :channel, :unique_prefix, :initiator_addtype
attr_reader :initiator_address_length, :acceptor_addrtype
attr_reader :acceptor_address_length

# Returns a channel binding hash acceptable for use as a AV_PAIR MsvAvChannelBindings
# field value as specified in the NTLM protocol
#
# @return [String] MD5 hash of gss_channel_bindings_struct
def channel_binding_token
@channel_binding_token ||= OpenSSL::Digest::MD5.new(gss_channel_bindings_struct).digest
end

def gss_channel_bindings_struct
@gss_channel_bindings_struct ||= begin
token = [initiator_addtype].pack('I')
token << [initiator_address_length].pack('I')
token << [acceptor_addrtype].pack('I')
token << [acceptor_address_length].pack('I')
token << [application_data.length].pack('I')
token << application_data
token
end
end

def channel_hash
@channel_hash ||= OpenSSL::Digest::SHA256.new(channel.to_der)
end

def application_data
@application_data ||= begin
data = unique_prefix
data << ':'
data << channel_hash.digest
data
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/net/ntlm/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ def initialize(username, password, opts = {})
end

# @return [NTLM::Message]
def init_context(resp = nil)
def init_context(resp = nil, channel_binding = nil)
if resp.nil?
@session = nil
type1_message
else
@session = Client::Session.new(self, Net::NTLM::Message.decode64(resp))
@session = Client::Session.new(self, Net::NTLM::Message.decode64(resp), channel_binding)
@session.authenticate!
end
end
Expand Down
20 changes: 17 additions & 3 deletions lib/net/ntlm/client/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ class Client::Session
CLIENT_TO_SERVER_SEALING = "session key to client-to-server sealing key magic constant\0"
SERVER_TO_CLIENT_SEALING = "session key to server-to-client sealing key magic constant\0"

attr_reader :client, :challenge_message
attr_reader :client, :challenge_message, :channel_binding

# @param client [Net::NTLM::Client] the client instance
# @param challenge_message [Net::NTLM::Message::Type2] server message
def initialize(client, challenge_message)
def initialize(client, challenge_message, channel_binding = nil)
@client = client
@challenge_message = challenge_message
@channel_binding = channel_binding
end

# Generate an NTLMv2 AUTHENTICATE_MESSAGE
Expand Down Expand Up @@ -213,11 +214,24 @@ def blob
b = Blob.new
b.timestamp = timestamp
b.challenge = client_challenge
b.target_info = challenge_message.target_info
b.target_info = target_info
b.serialize
end
end

def target_info
@target_info ||= begin
if channel_binding
t = Net::NTLM::TargetInfo.new(challenge_message.target_info)
av_id = Net::NTLM::TargetInfo::MSV_AV_CHANNEL_BINDINGS
t.av_pairs[av_id] = channel_binding.channel_binding_token
t.to_s
else
challenge_message.target_info
end
end
end

end
end
end
14 changes: 14 additions & 0 deletions lib/net/ntlm/exceptions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Net
module Ntlm
class NtlmError < StandardError; end

class InvalidTargetDataError < NtlmError
attr_reader :data

def initialize(msg, data)
@data = data
super(msg)
end
end
end
end
89 changes: 89 additions & 0 deletions lib/net/ntlm/target_info.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
module Net
module NTLM

# Represents a list of AV_PAIR structures
# @see https://msdn.microsoft.com/en-us/library/cc236646.aspx
class TargetInfo

# Allowed AvId values for an AV_PAIR
MSV_AV_EOL = "\x00\x00".freeze
MSV_AV_NB_COMPUTER_NAME = "\x01\x00".freeze
MSV_AV_NB_DOMAIN_NAME = "\x02\x00".freeze
MSV_AV_DNS_COMPUTER_NAME = "\x03\x00".freeze
MSV_AV_DNS_DOMAIN_NAME = "\x04\x00".freeze
MSV_AV_DNS_TREE_NAME = "\x05\x00".freeze
MSV_AV_FLAGS = "\x06\x00".freeze
MSV_AV_TIMESTAMP = "\x07\x00".freeze
MSV_AV_SINGLE_HOST = "\x08\x00".freeze
MSV_AV_TARGET_NAME = "\x09\x00".freeze
MSV_AV_CHANNEL_BINDINGS = "\x0A\x00".freeze

# @param av_pair_sequence [String] AV_PAIR list from challenge message
def initialize(av_pair_sequence)
@av_pairs = read_pairs(av_pair_sequence)
end

attr_reader :av_pairs

def to_s
result = ''
av_pairs.each do |k,v|
result << k
result << [v.length].pack('S')
result << v
end
result << Net::NTLM::TargetInfo::MSV_AV_EOL
result << [0].pack('S')
result.force_encoding(Encoding::ASCII_8BIT)
end

private

VALID_PAIR_ID = [
MSV_AV_EOL,
MSV_AV_NB_COMPUTER_NAME,
MSV_AV_NB_DOMAIN_NAME,
MSV_AV_DNS_COMPUTER_NAME,
MSV_AV_DNS_DOMAIN_NAME,
MSV_AV_DNS_TREE_NAME,
MSV_AV_FLAGS,
MSV_AV_TIMESTAMP,
MSV_AV_SINGLE_HOST,
MSV_AV_TARGET_NAME,
MSV_AV_CHANNEL_BINDINGS
].freeze

def read_pairs(av_pair_sequence)
offset = 0
result = {}
return result if av_pair_sequence.nil?

until offset >= av_pair_sequence.length
id = av_pair_sequence[offset..offset+1]

unless VALID_PAIR_ID.include?(id)
raise Net::Ntlm::InvalidTargetDataError.new(
"Invalid AvId #{to_hex(id)} in AV_PAIR structure",
av_pair_sequence
)
end

length = av_pair_sequence[offset+2..offset+3].unpack('S')[0].to_i
if length > 0
value = av_pair_sequence[offset+4..offset+4+length-1]
result[id] = value
end

offset += 4 + length
end

result
end

def to_hex(str)
return nil if str.nil?
str.bytes.map {|b| '0x' + b.to_s(16).rjust(2,'0').upcase}.join('-')
end
end
end
end
4 changes: 2 additions & 2 deletions lib/net/ntlm/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module NTLM
# @private
module VERSION
MAJOR = 0
MINOR = 5
TINY = 3
MINOR = 6
TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
Expand Down
17 changes: 17 additions & 0 deletions spec/lib/net/ntlm/channel_binding_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'spec_helper'

describe Net::NTLM::ChannelBinding do
let(:certificates_path) { 'spec/support/certificates' }
let(:sha_256_path) { File.join(certificates_path, 'sha_256_hash.pem') }
let(:sha_256_cert) { OpenSSL::X509::Certificate.new(File.read(sha_256_path)) }
let(:cert_hash) { "\x04\x0E\x56\x28\xEC\x4A\x98\x29\x91\x70\x73\x62\x03\x7B\xB2\x3C".force_encoding(Encoding::ASCII_8BIT) }

subject { Net::NTLM::ChannelBinding.create(sha_256_cert) }

describe '#channel_binding_token' do

it 'returns the correct hash' do
expect(subject.channel_binding_token).to eq cert_hash
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/net/ntlm/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
t2_challenge = "TlRMTVNTUAACAAAADAAMADgAAAA1goriAAyk1DmJUnUAAAAAAAAAAFAAUABEAAAABgLwIwAAAA9TAEUAUgBWAEUAUgACAAwAUwBFAFIAVgBFAFIAAQAMAFMARQBSAFYARQBSAAQADABzAGUAcgB2AGUAcgADAAwAcwBlAHIAdgBlAHIABwAIADd7mrNaB9ABAAAAAA=="
session = double("session")
expect(session).to receive(:authenticate!)
expect(Net::NTLM::Client::Session).to receive(:new).with(inst, instance_of(Net::NTLM::Message::Type2)).and_return(session)
expect(Net::NTLM::Client::Session).to receive(:new).with(inst, instance_of(Net::NTLM::Message::Type2), nil).and_return(session)
inst.init_context t2_challenge
end

Expand Down
76 changes: 76 additions & 0 deletions spec/lib/net/ntlm/target_info_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'spec_helper'

describe Net::NTLM::TargetInfo do
let(:key1) { Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME }
let(:value1) { 'some data' }
let(:key2) { Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME }
let(:value2) { 'some other data' }
let(:data) do
dt = key1.dup
dt << [value1.length].pack('S')
dt << value1
dt << key2.dup
dt << [value2.length].pack('S')
dt << value2
dt << Net::NTLM::TargetInfo::MSV_AV_EOL
dt << [0].pack('S')
dt.force_encoding(Encoding::ASCII_8BIT)
end

subject { Net::NTLM::TargetInfo.new(data) }

describe 'invalid data' do

context 'invalid pair id' do
let(:data) { "\xFF\x00" }

it 'returns an error' do
expect{subject}.to raise_error Net::Ntlm::InvalidTargetDataError
end
end
end

describe '#av_pairs' do

it 'returns the pair values with the given keys' do
expect(subject.av_pairs[key1]).to eq value1
expect(subject.av_pairs[key2]).to eq value2
end

context "target data is nil" do
subject { Net::NTLM::TargetInfo.new(nil) }

it 'returns the pair values with the given keys' do
expect(subject.av_pairs).to be_empty
end
end
end

describe '#to_s' do
let(:data) do
dt = key1.dup
dt << [value1.length].pack('S')
dt << value1
dt << key2.dup
dt << [value2.length].pack('S')
dt << value2
dt.force_encoding(Encoding::ASCII_8BIT)
end
let(:new_key) { Net::NTLM::TargetInfo::MSV_AV_CHANNEL_BINDINGS }
let(:new_value) { 'bindings' }
let(:new_data) do
dt = data
dt << new_key
dt << [new_value.length].pack('S')
dt << new_value
dt << Net::NTLM::TargetInfo::MSV_AV_EOL
dt << [0].pack('S')
dt.force_encoding(Encoding::ASCII_8BIT)
end

it 'returns bytes with any new data added' do
subject.av_pairs[new_key] = new_value
expect(subject.to_s).to eq new_data
end
end
end
19 changes: 19 additions & 0 deletions spec/support/certificates/sha_256_hash.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDKjCCAhKgAwIBAgIQV0qwsk2MCoxI2Do7IQ6eQzANBgkqhkiG9w0BAQsFADAa
MRgwFgYDVQQDDA8xOTIuMTY4LjEzNy4xNjEwHhcNMTYwMTI3MjIyMzA5WhcNMTcw
MTI3MjI0MzA5WjAaMRgwFgYDVQQDDA8xOTIuMTY4LjEzNy4xNjEwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+bGWZQFYjF+bV1WJ1L/MGVNmJR89aJ44Z
rKI/IXKFdbn5wjQPWng/DcaHR6xtMXQkc22boe58GK/uzl84ofbRa6qtboa5djdZ
9CGsd4Yf6CnVz4mhKSi+BnLi80ydhIRByxoX5bGcCSW6dixR5XiNMaMKzhCjQ+of
TU+PBNt7doXB7p0mO4AZz42v4rorRiPNasETj6wlKhFKCMvPLePTwphCgCQsLvgG
NQKtFD7TXvrZwplPSeCPhnzd1vHoZMisMn8ZVQ5dAfSEGGkPkOLO0htbUbdaNMoU
DPyo7Bu62Q/dqqo1MNbMYM5Ilw8mxe4drOs9UupH0eMovFhVMO0LAgMBAAGjbDBq
MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEw
GgYDVR0RBBMwEYIPMTkyLjE2OC4xMzcuMTYxMB0GA1UdDgQWBBSLuqyHonSmdm8m
9R+z2obO/X3/+TANBgkqhkiG9w0BAQsFAAOCAQEAH4pDGBclTHrwF+Bkbfj81ibK
E2SJSHbdhSx6YCsR28jXUOESfaik5ZPPMXscJqVc1FPpsU9clPFnGiAf0Kt48gsR
twfrRSGgJRv1ZgQyJ4dEQkXbQf2+8uY25Rv4kkFDSvPrE6E9o9Jf9bjqefUYski1
YoYdWzgrh/2qoNhnM34wizZgE1bWYbWA9MlUuWH9q/OBEx9uP/K53SXOR7DRzYcY
Kg1Z7hV86nvc0WutjEadgdtvJ7eUlg8vAWZqWo5SIdp69l0OEWUlHiaRsPImS5Hd
pX3W8n0wHCxBSntDww7U3SHg6DrYf72taBIQW7xFf63S37yLP4CNss68GqPdyQ==
-----END CERTIFICATE-----

0 comments on commit 1af7775

Please sign in to comment.