Skip to content
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

Adds GSSAPI Bind support #340

Closed
wants to merge 1 commit into from
Closed

Adds GSSAPI Bind support #340

wants to merge 1 commit into from

Conversation

dequbed
Copy link

@dequbed dequbed commented Sep 7, 2021

Adds the "GSSAPI" SASL Mechanism as bind option.

It does not add gss security layers and does not implement channel binding.

To use GSSAPI you call the GSSAPICCBind like so:

l.GSSAPICCBind("/path/to/your/krb5.conf", "/path/to/your/ccache", "<SPN of your LDAP server>")

On most *nix systems the krb5.conf is in /etc/krb5.conf and the ccache location can be found either via looking at the output of klist and is also usually stored in the environment variable $KRB5CCNAME.
Service Principal Names (SPN) of LDAP servers are usually of the form ldap/<hostname> so e.g. "ldap/example.com".

Closes #115.

This adds a new Mechanism for SASL Binds using GSSAPI. It does *not*
implement security layers. It does *not* implement any of the newer GS2
mechanism. It does *not* implement the KERBEROSV5 mechanism.
It also due to implementing GSSAPI specifically, not allow for channel
bindings.

Use this with caution.

Closes go-ldap#115.
@Adphi
Copy link

Adphi commented Sep 10, 2021

Thank you very much @dequbed for your work on this.
I did play a bit with your PR and had some troubles binding using GSSAPI, the error was:

checksum mismatch. Computed: 8fcf737614c1a8bba90b6a8f, Contained in token: 4c034eb84341924007010000

The token verification was throwing the error:

_, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL)
if err != nil {
	return nil, err
}

After some digging I found the GSSAPI specs about key derivation and wrapped token, the wrap token flags specs say that the flag's second bit is set if the token is sealed, my token flags value was 5 (0b00000101), indicating not sealed, so I added a check before the verification:

// verify if flags indicate seal (contains 0b10)
// https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.2
if (token.Flags & 0b10) != 0 {
	_, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL)
	if err != nil {
		return nil, err
	}
}

With this modifications it works !

But I don't know if this is the right fix, I don't have a deep knowledge about Kerberos and GSSAPI.

@dequbed
Copy link
Author

dequbed commented Sep 10, 2021

No, even if the wrap token isn't sealed (that is encrypted) KG-USAGE-ACCEPTOR-SEAL is still the correct keyusage. In fact, at this stage wrap tokens SHOULD NOT be encrypted at all since this exchange is to establish the security layer detailing exactly that.

There are a few reasons this could fail, the first one coming to mind would be that despite the server indicating it has signed the token with the established subkey (bit number 3 set) it has in fact signed it with the session key.

Not verifying the token also isn't a good solution here, although it only loses you mutual authentication.
Judging by the checksums you're using a SHA1 keytype or something even older. What's your Kerberos implementation on client & serverside? I have only tested it with MIT so there may be issues with other implementations.

@Adphi
Copy link

Adphi commented Sep 10, 2021

Thanks for you reply.

Judging by the checksums you're using a SHA1 keytype or something even older. What's your Kerberos implementation on client & serverside? I have only tested it with MIT so there may be issues with other implementations.

I am testing with:

  • Server: Samba Active Directory 4.12
  • Client:
      $ sudo apt list --installed|grep krb
    
      krb5-config/focal,focal,now 2.6ubuntu1 all [installed,automatic]
      krb5-locales/focal-updates,focal-updates,focal-security,focal-security,now 1.17-6ubuntu4.1 all [installed]
      krb5-multidev/focal-updates,focal-security,now 1.17-6ubuntu4.1 amd64 [installed,automatic]
      krb5-user/focal-updates,focal-security,now 1.17-6ubuntu4.1 amd64 [installed]
      libgssapi-krb5-2/focal-updates,focal-security,now 1.17-6ubuntu4.1 amd64 [installed]
      libkrb5-26-heimdal/focal,now 7.7.0+dfsg-1ubuntu1 amd64 [installed,automatic]
      libkrb5-3/focal-updates,focal-security,now 1.17-6ubuntu4.1 amd64 [installed]
      libkrb5-dev/focal-updates,focal-security,now 1.17-6ubuntu4.1 amd64 [installed]
      libkrb5support0/focal-updates,focal-security,now 1.17-6ubuntu4.1 amd64 [installed]
      sssd-krb5-common/now 2.2.3-3ubuntu0.4 amd64 [installed,upgradable to: 2.2.3-3ubuntu0.6]
      sssd-krb5/now 2.2.3-3ubuntu0.4 amd64 [installed,upgradable to: 2.2.3-3ubuntu0.6]

Copy link

@jdef jdef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now realize that I commented on the non-v3 code variant, but I actually tested my hacks on the v3/ package. quickly skimming, same comments probably apply to both. I tested against windows AD w/ a golang linux client built from a hacked version of this PR.


auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication")
auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "GSSAPI", "SASL Mech"))
auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(req.token[:]), "Credentials"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only append credentials child to auth if len(token) > 0

// Loop until we are done
done := false
OUTER:
for !done {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for !done && err == nil {
  done, err = func() (bool, error) {
    // do the real work here, so that defer is executed properly
  }()
}
return result, err


for _, child := range packet.Children[1].Children {
if child.ClassType == ber.ClassContext && child.Tag == 7 {
data, err = ioutil.ReadAll(child.Data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for err of ReadAll step?

return nil, err
}

token, err := spnego.NewKRB5TokenAPREQ(client, tkt, ekey, []int{gssapi.ContextFlagInteg, gssapi.ContextFlagConf, gssapi.ContextFlagMutual}, []int{})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed to include flags.APOptionMutualRequired (in the empty int list) for AD support

Flags: 0b100,
EC: uint16(encType.GetHMACBitLength() / 8),
RRC: 0,
SndSeqNum: 1,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe properly track this in "state" instead of hardcoding the seq here

}

token := &gssapi.WrapToken{}
err := token.Unmarshal(input, true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing against AD, this only works if I hacked the Unmarshal() call, which could be done here, after the fact, instead of within Unmarshal() itself, if needed. otherwise the call to token.Verify() fails w/ a checksum mismatch.

the hack: Unmarshal() parses header, payload, checksum in that order. to get this working with AD, I need to change the parse order: header, checksum, payload.

@jankirsten
Copy link

I've stumbled across #115 over a year ago and was very pleased when I found this PR today. Thank you very much @dequbed. I've tried to implement it myself but didn't get it working.

Based on GSSAPICCBind I was now able to implement GSSAPIKeytabBind which adds the possibility to use a keytab instead of username/PW.
It was successfully tested in a FreeIPA environment (MIT Kerberos, 389DS).

What needs to happen to get this PR merged? @johnweldon

@tooptoop4
Copy link

🏆

@tooptoop4
Copy link

@jankirsten can u share GSSAPIKeytabBind?
@jdef can u share AD changes?
@dequbed do u have example of how to retrieve the username?

@jdef
Copy link

jdef commented Nov 14, 2022

@tooptoop4 i'm unable to supply a proper patch (corporate red tape). i left a bunch of hints above, let me know if something is unclear w/ respect to what i've suggested

@tooptoop4
Copy link

@jdef how to retrieve the username?

@kingluo
Copy link

kingluo commented Mar 6, 2023

@jdef @dequbed @tooptoop4 @jankirsten

I'm setting up a windows AD domain service, and I need to access it via LDAP/GSSAPI/Kerberos.

But I encounter this issue:
ldap_sasl_interactive_bind_s: Local error (-2)
additional info: SASL(-1): generic failure: GSSAPI Error:
Unspecified GSS failure. Minor code may provide more information
(Message stream modified)

I could get the service ticket from Kerberos, but the ldap bind failed.

But I tried Linux kdc/openldap and that's ok.

Does it seem that there is somehow DNS issue? I don't know how to set
up a correct AD domain service. I just need to verify the simplest
ldap bind via Kerberos.

Do you have any idea where I am wrong?

Thank you very much!

@Denis-shl
Copy link

@kingluo Hello
Have you managed to fix this error ldap_sasl_interactive_bind_s: Local error (-2) ?
Maybe I have a solution to this problem. I need more information

When i call the function (c.GSSAPICCBind(confpath, cpath, spn)) i have an error ( LDAP Result Code 49 "Invalid Credentials": 8009030C: LdapErr: DSID-0C090579, comment: AcceptSecurityContext error, data 52e, v3839)
Have you ever encountered this error ?

@kingluo
Copy link

kingluo commented Mar 7, 2023

@kingluo Hello Have you managed to fix this error ldap_sasl_interactive_bind_s: Local error (-2) ? Maybe I have a solution to this problem. I need more information

Could you give some hints? Thanks. I managed to get the correct service ticket from Kerberos, but it seems that there is some problem between AD and KDC, because it happens at the first bind request between the client and AD. I search on google, but no solution was found, and somebody said it was related to the DNS setting. I am a newbie to windows AD, so do you have experience with this topic?

When i call the function (c.GSSAPICCBind(confpath, cpath, spn)) i have an error ( LDAP Result Code 49 "Invalid Credentials": 8009030C: LdapErr: DSID-0C090579, comment: AcceptSecurityContext error, data 52e, v3839) Have you ever encountered this error ?

No, I did not have such an issue. Maybe the service ticket is invalid?

@Denis-shl
Copy link

@kingluo
From which environment are you trying to connect linux/Windows?You get a Kerberos ticket through kinit?

@kingluo
Copy link

kingluo commented Mar 7, 2023

@kingluo From which environment are you trying to connect linux/Windows?You get a Kerberos ticket through kinit?

I uses a newly installed Windows Server 2022 Datacenter on the cloud and tried to install and configure AD/DNS from the server manager. The AD works well as a pure LDAP server, I could do simple bind and search to the AD.

But when I turn to use Kerberos, it failed. I could get a Kerberos ticket via kinit and I use klist to confirm the TGT.
Using the same procedure, I could do a successful GSSAPI bind to MIT Kerberos KDC and openldap, but no luck on AD.

@Denis-shl
Copy link

Denis-shl commented Mar 7, 2023

I'm sorry I have difficulties with translation. I don't understand what your problem is yet. I have a little experience in setting up windows AD.

Do you want to use the go-ldap library to access windows AD over the kerberos protocol ?

We can use another means of communication telegram/email ?

@kingluo
Copy link

kingluo commented Mar 7, 2023

@Denis-shl Is there any setting that should be done for a Linux client to access AD/Kerberos? For example, add IP in the DNS server on the AD side, add SPN on the AD side, etc.
Yes, if you wish, we could talk via mail. My mail is luajit.io@gmail.com

@kingluo
Copy link

kingluo commented Mar 8, 2023

@dequbed @Adphi By Bypassing the checksum verification, I could do a successful GSSAPI bind to Windows AD.
But from Heimdal implementation, the checksum still gets verified even if the Sealed flag is not set, but why Heimdal could verify the checksum without a problem?

https://github.com/heimdal/heimdal/blob/dffa545f81fa11993b4be1713bc1ee470c4b9fd1/lib/gssapi/krb5/cfx.c#L1013

@jdef From Heimdal implementation, the checksum seems to be following the payload, and I did really use Heimdal GSSAPI to access AD successfully (via bonsai or ldapsearch). So I'm confused why you said the wrap token from AD is special in the checksum/payload order.

The payload should be followed by the checksum, which could be confirmed from the code:

https://github.com/heimdal/heimdal/blob/dffa545f81fa11993b4be1713bc1ee470c4b9fd1/lib/gssapi/krb5/cfx.c#L992-L997

The RFC also has the same description:

https://www.rfc-editor.org/rfc/rfc4121.html#section-4.2.6.2

16..last Data Encrypted data for Wrap tokens with
confidentiality, or plaintext data followed
by the checksum for Wrap tokens without
confidentiality, as described in section
4.2.4.

@Denis-shl Thanks for your share. After some changes (I don't know which change is the key reason), I could use go-ldap to access AD over Kerberos.

Here is my demo:

https://gist.github.com/kingluo/7adbe1f27c3b952592d1aa89120a2f52

Note that I bypass the checksum verification, which should be a security risk, so I don't think it should be used in the production environment.

@santisis
Copy link

santisis commented Apr 16, 2023

@jankirsten can u share GSSAPIKeytabBind?

I've also implemented that function as:

// GSSAPI Bind using keytab
func (l *Conn) GSSAPIKeytabBind(username, realm, confpath, keytabpath, spn string) error {
        config, err := k5conf.Load(confpath)
        if err != nil {
                return err
        }

        keytab, err := k5keytab.Load(keytabpath)
        if err != nil {
                return err
        }

        client := gssapi.NewWithKeytab(username, realm, keytab, config)

        req := &GSSAPIBindRequest{
                SPN:     spn,
                client:  client,
        }
        _, err = l.GSSAPIBind(req)
        return err
}

It's required to add k5keytab "github.com/jcmturner/gokrb5/v8/keytab" as an import at the beggining.

An example of use is:

err = l.GSSAPIKeytabBind("host/host.example.com", "EXAMPLE.COM", "/etc/krb5.conf", "/etc/krb5.keytab", "ldap/ldap.example.com")

@stephen-jiadawang
Copy link

@kingluo
I am also facing very similar issue as you mentioned in
#340 (comment)

could you let me know how you addressed this issue?
you mentioned about bypass checksum, is it some issue on Linux client side (krb5 lib)?
or settings on AD server?

@kingluo
Copy link

kingluo commented Jun 24, 2023

@kingluo I am also facing very similar issue as you mentioned in #340 (comment)

could you let me know how you addressed this issue? you mentioned about bypass checksum, is it some issue on Linux client side (krb5 lib)? or settings on AD server?

gokrb5 has no problem, IMO, but lacks negotiating flow implementation, which is to be done in go-ldap.
In the negotiation, the checksum returned by Windows AD is special, but anyways it should be checked, not skipped.

No special setting is needed in setting up the Windows ADDS, and no DNS. Everything is step-by-step by the wizard.
I could use bonsai (which in turn uses Heimdal lib) to access Windows ADDS without a problem.

@kingluo
Copy link

kingluo commented Jun 25, 2023

@stephen-jiadawang
Check this post for my setting up Windows ADDS:
http://luajit.io/posts/access-windows-adds-kerberos-from-openresty/

@levkohimins
Copy link
Contributor

New implementation with latest master code here.

@patryk4815
Copy link

PR can be closed
see: #115 (comment)
implemented in PR: #449

@cpuschma cpuschma closed this Mar 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GSSAPI support