ruby-ldap / ruby-net-ldap

Pure Ruby LDAP library
https://rubygems.org/gems/net-ldap
Other
399 stars 253 forks source link

Unable to connect when LDAP Channel Binding is enabled #339

Open sukeerthiadiga opened 4 years ago

sukeerthiadiga commented 4 years ago

Trying to connect to AD by enforcing the LDAP Channel Binding ()

Ending up with the below error

=> #<Net::LDAP::PDU:0x0000000013568ea8
  @app_tag=1,
  @ldap_controls=[],
  @ldap_result=
      {:resultCode=>49,
       :matchedDN=>"",
       :errorMessage=>"80090346: LdapErr: DSID-0C09056D, comment: AcceptSecurityContext error, data 80090346, v2580\u0000"},
  @message_id=1>
artmotion commented 4 years ago

Hi! I just stumbled upon this. Any update or progress on this topic :)

smlsml commented 4 years ago

We've been trying to make this work as well. We use GSS-SPNEGO and code that looks like this:

    def sasl_gss_spnego(user, password, domain=nil)
      raise Net::LDAP::Error, "Invalid binding information" unless user && password

      challenge_response = proc do |challenge|
        challenge.force_encoding(Encoding::BINARY)
        t2_msg               = Net::NTLM::Message.parse(challenge)
        auth_params          = {:user => user, :password => password}
        auth_params[:domain] = domain unless domain.blank?
        t3_msg               = t2_msg.response(auth_params, {:ntlmv2 => true})
        t3_msg.user.force_encoding(Encoding::BINARY)
        t3_msg.serialize
      end

      {
        :mechanism          => "GSS-SPNEGO",
        :initial_credential => Net::NTLM::Message::Type1.new.serialize,
        :challenge_response => challenge_response,
        :method             => :sasl
      }
    end

Using the default Type1 flags (Net::NTLM::Message::Type1.new), it does not work. If I manipulate the flags, I can get it to be successful when I ask for sign and seal, but when watching it on wireshark, I don't see how it is any different than the default flags.

I have yet to require channel binding and test it however: https://support.microsoft.com/en-us/help/4034879/how-to-add-the-ldapenforcechannelbinding-registry-entry

fwininger commented 4 years ago

I will also try your code.

Microsoft enforce channel binding or ldaps with the KB of march 2020.

https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirement-for-windows

fwininger commented 4 years ago

I have the same issue AcceptSecurityContext error :cry:

fwininger commented 4 years ago

According the documentation here : https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes/#rc-invalidCredentials

invalidCredentials (49) Applicable operation types: bind The invalidCredentials result code indicates that the client attempted to bind with a set of credentials that cannot be used to authenticate. Some of the potential reasons that this result code might be returned are:

  • The bind request targeted a user that does not exist.
  • The client tried to authenticate with an incorrect password.
  • The client tried to authenticate with a SASL bind request that included non-password credentials that could not be successfully verified.
  • The bind request targeted a user that is not permitted to authenticate for some reason (for example, because the account has been locked, the user’s password has expired, etc.).
fwininger commented 4 years ago

I have done the implementation of the net-ldap-gss-spnego gem.

It's work for me with default AD settings.

https://github.com/fwininger/ruby-net-ldap-gss-spnego

raj-sharan commented 4 years ago

To handle the channel binding you can use Net::NTLM::Client, it creates type3 message using challenge(type2) and channel binding. For creating channel binding you need server certificate that can be fetched from the type1 response or @connection

ntlm_client = Net::NTLM::Client.new(user, pass, {domain: "XYZ"}) binding = @connection.peer_cert.nil? ? nil : Net::NTLM::ChannelBinding.create(@connection.peer_cert) type3 = ntlm_client.init_context(Base64.encode64(type2_challenge), binding)

bmalets commented 4 years ago

@raj-sharan could you please "beautify" your code example? it's a bit hard to understand where exactly this defined type3 message needs to be added :slightly_smiling_face:

bmalets commented 4 years ago

I tried ruby-net-ldap-gss-spnego gem, but it does not fully work for me:

If on LDAP server-side, configuration flag EnforceChannelBinding is set to 1 - authentication using "ruby-net-ldap-gss-spnego" adapter works fine.

But, according to Microsoft recommendations EnforceChannelBinding value should be changed to 2. In this case, authentication is broken: LDAP server returns "LDAP Error-code: 49 (Invalid Credentials)" without more details. (username and password are 100% correct, tested multiple times with multiple users)

BTW, Is SSL/TLS config mandatory for ChannelBinding? Should it work without SSL certificates? Using Ldap::Client these encryption options etc:

encryption: {
  method: :simple_tls,
  tls_options: {verify_mode: OpenSSL::SSL::VERIFY_NONE}
}
fwininger commented 4 years ago

I tried ruby-net-ldap-gss-spnego gem, but it does not fully work for me:

If on LDAP server-side, configuration flag EnforceChannelBinding is set to 1 - authentication using "ruby-net-ldap-gss-spnego" adapter works fine.

But, according to Microsoft recommendations EnforceChannelBinding value should be changed to 2. In this case, authentication is broken: LDAP server returns "LDAP Error-code: 49 (Invalid Credentials)" without more details. (username and password are 100% correct, tested multiple times with multiple users)

Thanks for this comment, I don't have time this week to fix the ruby-net-ldap-gss-spnego but I open for any PR to solve this issue.

smlsml commented 4 years ago

Channel binding is an SSL/TLS concept only.

LDAP signing and sealing is a non-secure only concept too.

On Thu, Jun 18, 2020 at 3:26 AM Bohdan Malets notifications@github.com wrote:

I tried ruby-net-ldap-gss-spnego gem, but it does not fully work for me:

If on LDAP server-side, configuration flag EnforceChannelBinding is set to 1 - authentication using "ruby-net-ldap-gss-spnego" adapter works fine.

But, according to Microsoft recommendations https://support.microsoft.com/en-us/help/4034879/how-to-add-the-ldapenforcechannelbinding-registry-entry EnforceChannelBinding value should be changed to 2. In this case, authentication is broken: LDAP server returns "LDAP Error-code: 49 (Invalid Credentials)" without more details

BTW, Is SSL/TLS config mandatory for ChannelBinding? Should it work without SSL certificates? Using Ldap::Client these encryption options etc:

encryption: { method: :timple_tls, tls_options: {verify_mode: OpenSSL::SSL::VERIFY_NONE} }

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ruby-ldap/ruby-net-ldap/issues/339#issuecomment-645929293, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACXAZGT23B2NCDTPH7QLRTRXHTTRANCNFSM4J33PZEA .

bmalets commented 4 years ago

As Channel binding is an SSL/TLS concept only - I added SSL/TLS settings to my LDAP configurations. Unfortunately, I am not "MS guru" to understand and even get more verbose logs from LDAP server side :( But here is a more detailed example when signing and binding don't want to work together:

require 'net/ldap'
require 'net/ldap/auth_adapter/gss_spnego'

ldap_options = {
  hosts:  [
    ['ldap_hostname.lan', 636]
  ],
  base: 'DC=ldap_hostname,DC=lan',
  encryption: {
    method: :simple_tls,
    tls_options: {
      ca_file: '/full/path/to/cert.pem',
      ssl_version: 'TLSv1_2'
    }
  },
  auth: {
    auth_method: 'GSS-SPNEGO',
    username: 'bmalets', # username, not DN
    password: 'password'
  }
}

ldap = Net::LDAP.new(ldap_options)

# When LDAP server registry settings are:
# LDAPServerIntegrity       = 2 (Always sign (Level 2))
# LdapEnforceChannelBinding = 1 (DWORD value: 1 indicates enabled, when supported. All clients that are running on a version of Windows that has been updated to support channel binding tokens (CBT) must provide channel binding information to the server. Clients that are running a version of Windows that has not been updated to support CBT do not have to do so. This is an intermediate option that allows for application compatibility.)

ldap.bind
ldap.get_operation_result # => 0. Success

# When LDAP server registry settings are:
# LDAPServerIntegrity       = 2 (Always sign (Level 2))
# LdapEnforceChannelBinding = 2 (DWORD value: 2 indicates enabled, always. All clients must provide channel binding information. The server rejects authentication requests from clients that do not do so.)

ldap.bind
ldap.get_operation_result # => Error-code: 49. Invalid Credentials
raj-sharan commented 4 years ago

@raj-sharan could you please "beautify" your code example? it's a bit hard to understand where exactly this defined type3 message needs to be added 🙂

@bmalets, You should try this It should work with LdapEnforceChannelBinding = 2

require 'net/ldap'
require 'net/ntlm'
require 'net/ldap/auth_adapter/gss_spnego'

#Patch bind method to share the peer_cert
module Net
  class LDAP
    class AuthAdapter
      class Sasl

        # Patch this method
        def bind(auth)
          ...........
          ...........
          n = 0
          loop do
            ..........
            ..........

            # Instead
            # cred = chall.call(pdu.result_server_sasl_creds)
            # Add following code

            cred =
              if @connection.socket.respond_to?(:peer_cert)
                chall.call(pdu.result_server_sasl_creds, @connection.socket.peer_cert)
              else
                chall.call(pdu.result_server_sasl_creds)
              end
          end

          raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
        end
      end
    end
  end
end

def sasl_gss_spnego(credential)
  challenge_response = proc do |challenge, peer_cert|
    challenge.force_encoding(Encoding::BINARY)
    ntlm_client       = Net::NTLM::Client.new(credential[:user], credential[:password], {domain: credential[:domain]})
    encoded_challenge = Base64.encode64(challenge)
    channel_binding   = peer_cert ? Net::NTLM::ChannelBinding.create(peer_cert) : nil

    t3_msg = ntlm_client.init_context(encoded_challenge, channel_binding)

    t3_msg.user.force_encoding(Encoding::BINARY)
    t3_msg.serialize
  end

  {
    :mechanism          => "GSS-SPNEGO",
    :initial_credential => Net::NTLM::Message::Type1.new.serialize,
    :challenge_response => challenge_response,
    :method             => :sasl
  }
end

ldap_options = {
  hosts:  [
            ['ldap_hostname.lan', 636]
          ],
  base: 'DC=ldap_hostname,DC=lan',
  encryption: {
    method: :simple_tls,
    tls_options: {
      ca_file: '/full/path/to/cert.pem',
      ssl_version: 'TLSv1_2'
    }
  },
  auth: sasl_gss_spnego({:user => "bmalets", :password => "password", :domain => 'DOMAIN_NAME'})
}

ldap = Net::LDAP.new(ldap_options)

ldap.bind
ldap.get_operation_result
smlsml commented 4 years ago

LDAPServerIntegrity and LdapEnforceChannelBinding are mutually exclusive. The reason your example didn't work is you changed LdapEnforceChannelBinding from 1 to 2. If you repeat that test with only changing LDAPServerIntegrity from 1 to 2 but leaving LdapEnforceChannelBinding at 1, it will work and prove that LDAPServerIntegrity doesn't matter when using SSL/TLS (which is what MS says is the case).

On Mon, Jun 22, 2020 at 3:52 AM Bohdan Malets notifications@github.com wrote:

More detailed example when signing and binding don't want to work together:

require 'net/ldap'require 'net/ldap/auth_adapter/gss_spnego' ldap_options = { hosts: [ ['ldap_hostname.lan', 636] ], base: 'DC=ldap_hostname,DC=lan', encryption: { method: :simple_tls, tls_options: { ca_file: '/full/path/to/cert.pem', ssl_version: 'TLSv1_2' } }, auth: { auth_method: :gss_spnego, username: 'bmalets' # username, not DN password: 'password' }} ldap = Net::LDAP.new(ldap_options)

When LDAP server registry settings are:# LDAPServerIntegrity = 2 (Always sign (Level 2))# LdapEnforceChannelBinding = 1 (DWORD value: 1 indicates enabled, when supported. All clients that are running on a version of Windows that has been updated to support channel binding tokens (CBT) must provide channel binding information to the server. Clients that are running a version of Windows that has not been updated to support CBT do not have to do so. This is an intermediate option that allows for application compatibility.)

ldap.bindldap.get_operation_result # => 0. Success

When LDAP server registry settings are:# LDAPServerIntegrity = 2 (Always sign (Level 2))# LdapEnforceChannelBinding = 2 (DWORD value: 2 indicates enabled, always. All clients must provide channel binding information. The server rejects authentication requests from clients that do not do so.)

ldap.bindldap.get_operation_result # => Error-code: 49. Invalid Credentials

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ruby-ldap/ruby-net-ldap/issues/339#issuecomment-647441592, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACXAZH7V2CLWTXSMJWWVYDRX4ZYLANCNFSM4J33PZEA .

bmalets commented 4 years ago

@smlsml, thank you for your answer. Did not find any info in Windows Server documentation that LDAPServerIntegrity and LdapEnforceChannelBinding are mutually exclusive... Could you please share some links about this if you have one?

I got only one page about MS update recommendations with this message:

To maximize compatibility with older operating system versions (Windows Server 2008 and earlier versions), we recommend that you enable this setting with a value of 1.

But LDAP server that I am trying to connect is MS 2012, so it definitely does not affect my case.

So, "49. Invalid Credentials" response is an expected behavior when LDAPServerIntegrity = 2 and LdapEnforceChannelBinding = 2. And there are only two ways to make it work:

Is it correct? :thinking: