From 5cdfb51a66791c0a1058083e336088b0339922d2 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Tue, 16 Feb 2016 09:20:31 -0800 Subject: [PATCH] Support Extended Protection for Authentication (aka Channel binding) --- .travis.yml | 1 - lib/net/ntlm.rb | 3 + lib/net/ntlm/channel_binding.rb | 65 ++++++++++++++++ lib/net/ntlm/client.rb | 4 +- lib/net/ntlm/client/session.rb | 20 ++++- lib/net/ntlm/exceptions.rb | 14 ++++ lib/net/ntlm/target_info.rb | 89 ++++++++++++++++++++++ lib/net/ntlm/version.rb | 4 +- spec/lib/net/ntlm/channel_binding_spec.rb | 17 +++++ spec/lib/net/ntlm/client_spec.rb | 2 +- spec/lib/net/ntlm/target_info_spec.rb | 76 ++++++++++++++++++ spec/support/certificates/sha_256_hash.pem | 19 +++++ 12 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 lib/net/ntlm/channel_binding.rb create mode 100644 lib/net/ntlm/exceptions.rb create mode 100644 lib/net/ntlm/target_info.rb create mode 100644 spec/lib/net/ntlm/channel_binding_spec.rb create mode 100644 spec/lib/net/ntlm/target_info_spec.rb create mode 100644 spec/support/certificates/sha_256_hash.pem diff --git a/.travis.yml b/.travis.yml index a75bfd7..fb7541f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: ruby rvm: - 1.9.3 - 1.9.2 - - 1.8.7 - 2.0.0 - rbx-19mode - rbx-18mode diff --git a/lib/net/ntlm.rb b/lib/net/ntlm.rb index c269440..08b9820 100644 --- a/lib/net/ntlm.rb +++ b/lib/net/ntlm.rb @@ -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' @@ -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 diff --git a/lib/net/ntlm/channel_binding.rb b/lib/net/ntlm/channel_binding.rb new file mode 100644 index 0000000..fda9c63 --- /dev/null +++ b/lib/net/ntlm/channel_binding.rb @@ -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 diff --git a/lib/net/ntlm/client.rb b/lib/net/ntlm/client.rb index bb783a8..91d0895 100644 --- a/lib/net/ntlm/client.rb +++ b/lib/net/ntlm/client.rb @@ -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 diff --git a/lib/net/ntlm/client/session.rb b/lib/net/ntlm/client/session.rb index d88664f..d5dbc2f 100644 --- a/lib/net/ntlm/client/session.rb +++ b/lib/net/ntlm/client/session.rb @@ -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 @@ -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 diff --git a/lib/net/ntlm/exceptions.rb b/lib/net/ntlm/exceptions.rb new file mode 100644 index 0000000..12f5201 --- /dev/null +++ b/lib/net/ntlm/exceptions.rb @@ -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 diff --git a/lib/net/ntlm/target_info.rb b/lib/net/ntlm/target_info.rb new file mode 100644 index 0000000..7cc50ac --- /dev/null +++ b/lib/net/ntlm/target_info.rb @@ -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 diff --git a/lib/net/ntlm/version.rb b/lib/net/ntlm/version.rb index 80e50d1..2ceec71 100644 --- a/lib/net/ntlm/version.rb +++ b/lib/net/ntlm/version.rb @@ -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 diff --git a/spec/lib/net/ntlm/channel_binding_spec.rb b/spec/lib/net/ntlm/channel_binding_spec.rb new file mode 100644 index 0000000..c1719b7 --- /dev/null +++ b/spec/lib/net/ntlm/channel_binding_spec.rb @@ -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 diff --git a/spec/lib/net/ntlm/client_spec.rb b/spec/lib/net/ntlm/client_spec.rb index 1544603..ef99d62 100644 --- a/spec/lib/net/ntlm/client_spec.rb +++ b/spec/lib/net/ntlm/client_spec.rb @@ -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 diff --git a/spec/lib/net/ntlm/target_info_spec.rb b/spec/lib/net/ntlm/target_info_spec.rb new file mode 100644 index 0000000..a6e8a1e --- /dev/null +++ b/spec/lib/net/ntlm/target_info_spec.rb @@ -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 diff --git a/spec/support/certificates/sha_256_hash.pem b/spec/support/certificates/sha_256_hash.pem new file mode 100644 index 0000000..03cbed5 --- /dev/null +++ b/spec/support/certificates/sha_256_hash.pem @@ -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-----