简体   繁体   English

在 Samba 4.11 上使用多个 SPN 和 AES256 进行 Kerberos SPNEGO 身份验证

[英]Kerberos SPNEGO Authentication with multiple SPN and AES256 on Samba 4.11

I have a HTTP service written in Kotlin and using Tomcat that listens in multiple domains and those need to be authenticated via Kerberos.我有一个用 Kotlin 编写的 HTTP 服务,并使用 Tomcat 在多个域中侦听并且需要通过 Kerberos 进行身份验证。 On Samba 4.9 we had a user with multiple SPNs with AES256 encryption enabled.在 Samba 4.9 上,我们有一个用户拥有多个启用了 AES256 加密的 SPN。 A keytab was generated for that user containing all SPNs.为该用户生成了一个包含所有 SPN 的密钥表。

After upgrading to Samba 4.11, multiple SPNs in a single user stoped to work.升级到 Samba 4.11 后,单个用户中的多个 SPN 停止工作。 The error Client 'HTTP/a.example.com@CORP.EXAMPLE.COM' not found in Kerberos database while getting initial credentials was thrown. Client 'HTTP/a.example.com@CORP.EXAMPLE.COM' not found in Kerberos database while getting initial credentials We fixed that by creating multiple users, one for each SPN and setting the UPN to the value of the single SPN.我们通过创建多个用户来解决此问题,每个 SPN 一个用户并将 UPN 设置为单个 SPN 的值。 After that we generated keytabs for each user and then we merged it.之后,我们为每个用户生成了 keytab,然后我们将其合并。

The problem is that when I receive a ticket with aes256-cts-hmac-sha1-96 , java.security.GeneralSecurityException: Checksum failed is thrown and just works in one domain, the one I use as principal.问题是,当我收到一张带有aes256-cts-hmac-sha1-96 , java.security.GeneralSecurityException: Checksum failed的票证时,它只在一个域中工作,即我用作主体的域。 arcfour-hmac-md5 works just fine on all domains, but I need to support AES encryption. arcfour-hmac-md5在所有域上都可以正常工作,但我需要支持 AES 加密。

I've tested this scenario on our old Samba 4.9 and the same happens.我已经在我们的旧 Samba 4.9 上测试了这个场景,同样的情况也发生了。 If we have multiple users with a single SPN each and a keytab with all of them, Checksum failed is also thrown.如果我们有多个用户,每个用户都有一个 SPN,并且每个用户都有一个 keytab,那么也会抛出Checksum failed

So either I manage to have a single user with multiple SPNs working on Samba 4.11 or I have to get rid of the Checksum failed when using AES encryption.因此,要么我设法让一个具有多个 SPN 的用户在 Samba 4.11 上工作,要么我必须在使用 AES 加密时摆脱Checksum failed

java -version java-版本

openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment 18.9 (build 11.0.6+10)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.6+10, mixed mode)

JAVA_OPTS JAVA_OPTS

-Dsun.security.krb5.disableReferrals=true
-Dsun.security.krb5.debug=true
-Dsun.security.spnego.debug=true

.java.login.config .java.login.config

example {
    com.sun.security.auth.module.Krb5LoginModule required
            keyTab="/root/HTTP.keytab"
            principal="HTTP/a.example.com@CORP.EXAMPLE.COM"
            debug=true
            storeKey=true
            useKeyTab=true;
};

HTTP.keytab HTTP.keytab

Vno  Type                     Principal                          
  2  aes256-cts-hmac-sha1-96  HTTP/a.example.com@CORP.EXAMPLE.COM
  2  aes128-cts-hmac-sha1-96  HTTP/a.example.com@CORP.EXAMPLE.COM
  2  arcfour-hmac-md5         HTTP/a.example.com@CORP.EXAMPLE.COM
  2  des-cbc-md5-deprecated   HTTP/a.example.com@CORP.EXAMPLE.COM
  2  des-cbc-crc-deprecated   HTTP/a.example.com@CORP.EXAMPLE.COM
  2  aes256-cts-hmac-sha1-96  HTTP/b.example.com@CORP.EXAMPLE.COM
  2  aes128-cts-hmac-sha1-96  HTTP/b.example.com@CORP.EXAMPLE.COM
  2  arcfour-hmac-md5         HTTP/b.example.com@CORP.EXAMPLE.COM
  2  des-cbc-md5-deprecated   HTTP/b.example.com@CORP.EXAMPLE.COM
  2  des-cbc-crc-deprecated   HTTP/b.example.com@CORP.EXAMPLE.COM

HealthServlet.kt HealthServlet.kt

import org.ietf.jgss.GSSCredential
import org.ietf.jgss.GSSManager
import org.ietf.jgss.Oid
import java.io.IOException
import java.security.PrivilegedActionException
import java.security.PrivilegedExceptionAction
import java.util.Base64
import javax.security.auth.Subject
import javax.security.auth.login.LoginContext
import javax.security.auth.login.LoginException
import javax.servlet.ServletException
import javax.servlet.annotation.WebServlet
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@WebServlet("/healthz")
class HealthServlet : HttpServlet() {
    @Throws(ServletException::class, IOException::class)
    override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
        val authorization = req.getHeader("Authorization") ?: let {
            resp.addHeader("WWW-Authenticate", "Negotiate")
            resp.status = HttpServletResponse.SC_UNAUTHORIZED
            return
        }

        val negotiate = authorization.substringAfter(' ')
        val token = Base64.getDecoder().decode(negotiate)

        // Get own Kerberos credentials for accepting connection
        val manager = GSSManager.getInstance()
        val spnegoOid = Oid("1.3.6.1.5.5.2")

        var serverCreds: GSSCredential? = null
        this.loginAndAction(PrivilegedExceptionAction {
            serverCreds = manager.createCredential(null, GSSCredential.DEFAULT_LIFETIME, spnegoOid, GSSCredential.ACCEPT_ONLY)
        })

        val context = manager.createContext(serverCreds as GSSCredential)

        val respToken = context!!.acceptSecContext(token, 0, token.size)
        val respNegotiate = Base64.getEncoder().encodeToString(respToken)

        // Send a token to the peer if one was generated by
        // acceptSecContext
        if (respToken != null) {
            System.err.println("Will send token of size " + token.size + " from acceptSecContext.")

            resp.addHeader("WWW-Authenticate", "Negotiate $respNegotiate")
            resp.status = HttpServletResponse.SC_OK

            resp.writer.println(context.srcName)
        }

        System.err.println("Context Established! ")
        System.err.println("Client principal is " + context.srcName)
        System.err.println("Server principal is " + context.targName)

        /*
         * If mutual authentication did not take place, then
         * only the client was authenticated to the
         * server. Otherwise, both client and server were
         * authenticated to each other.
         */
        if (context.mutualAuthState)
            System.err.println("Mutual authentication took place!")
    }

    @Throws(LoginException::class, PrivilegedActionException::class)
    private fun <T> loginAndAction(action: PrivilegedExceptionAction<T>) {
        val context = LoginContext("example")
        context.login()

        // Perform action as authenticated user
        val subject = context.subject
        println(subject)

        Subject.doAs(subject, action)
        context.logout()
    }
}

Log日志

Debug is  true storeKey true useTicketCache false useKeyTab true doNotPrompt false ticketCache is null isInitiator true KeyTab is /root/HTTP.keytab refreshKrb5Config is false principal is HTTP/a.example.com@CORP.EXAMPLE.COM tryFirstPass is false useFirstPass is false storePass is false clearPass is false
Looking for keys for: HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (1) for HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (3) for HTTP/a.example.com@CORP.EXAMPLE.COM
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Looking for keys for: HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (1) for HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (3) for HTTP/a.example.com@CORP.EXAMPLE.COM
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 20 19 16 23.
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=dc1.corp.example.com. UDP:88, timeout=30000, number of retries =3, #bytes=175
>>> KDCCommunication: kdc=dc1.corp.example.com. UDP:88, timeout=30000,Attempt =1, #bytes=175
>>> KrbKdcReq send: #bytes read=315
>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 16

>>>Pre-Authentication Data:
     PA-DATA type = 15

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = CORP.EXAMPLE.COMa, s2kparams = 0000: 00 00 10 00                                        ....

>>> KdcAccessibility: remove dc1.corp.example.com.:88
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
     sTime is Thu May 21 20:14:03 UTC 2020 1590092043000
     suSec is 748632
     error code is 25
     error Message is Additional pre-authentication required
     crealm is CORP.EXAMPLE.COM
     cname is HTTP/a.example.com@CORP.EXAMPLE.COM
     sname is krbtgt/CORP.EXAMPLE.COM@CORP.EXAMPLE.COM
     eData provided.
     msgType is 30
>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 16

>>>Pre-Authentication Data:
     PA-DATA type = 15

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = CORP.EXAMPLE.COMa, s2kparams = 0000: 00 00 10 00                                        ....

KRBError received: Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ
KrbAsReqBuilder: PREAUTH FAILED/REQ, re-send AS-REQ
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 20 19 16 23.
Looking for keys for: HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (1) for HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (3) for HTTP/a.example.com@CORP.EXAMPLE.COM
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Looking for keys for: HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (1) for HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (3) for HTTP/a.example.com@CORP.EXAMPLE.COM
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 20 19 16 23.
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=dc1.corp.example.com. UDP:88, timeout=30000, number of retries =3, #bytes=264
>>> KDCCommunication: kdc=dc1.corp.example.com. UDP:88, timeout=30000,Attempt =1, #bytes=264
>>> KrbKdcReq send: #bytes read=199
>>> KrbKdcReq send: kdc=dc1.corp.example.com. TCP:88, timeout=30000, number of retries =3, #bytes=264
>>> KDCCommunication: kdc=dc1.corp.example.com. TCP:88, timeout=30000,Attempt =1, #bytes=264
>>>DEBUG: TCPClient reading 1511 bytes
>>> KrbKdcReq send: #bytes read=1511
>>> KdcAccessibility: remove dc1.corp.example.com.:88
Looking for keys for: HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (1) for HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (3) for HTTP/a.example.com@CORP.EXAMPLE.COM
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsRep cons in KrbAsReq.getReply HTTP/a.example.com
principal is HTTP/a.example.com@CORP.EXAMPLE.COM
Will use keytab
Commit Succeeded 

Subject:
    Principal: HTTP/a.example.com@CORP.EXAMPLE.COM
    Private Credential: Ticket (hex) = 
... REDACTED ...

Client Principal = HTTP/a.example.com@CORP.EXAMPLE.COM
Server Principal = krbtgt/CORP.EXAMPLE.COM@CORP.EXAMPLE.COM
Session Key = EncryptionKey: keyType=18 keyBytes (hex dump)=
... REDACTED ...


Forwardable Ticket false
Forwarded Ticket false
Proxiable Ticket false
Proxy Ticket false
Postdated Ticket false
Renewable Ticket false
Initial Ticket true
Auth Time = Thu May 21 20:14:03 UTC 2020
Start Time = Thu May 21 20:14:03 UTC 2020
End Time = Fri May 22 06:14:03 UTC 2020
Renew Till = null
Client Addresses  Null 
    Private Credential: /root/HTTP.keytab for HTTP/a.example.com@CORP.EXAMPLE.COM

Found KeyTab /root/HTTP.keytab for HTTP/a.example.com@CORP.EXAMPLE.COM
Found KeyTab /root/HTTP.keytab for HTTP/a.example.com@CORP.EXAMPLE.COM
Found ticket for HTTP/a.example.com@CORP.EXAMPLE.COM to go to krbtgt/CORP.EXAMPLE.COM@CORP.EXAMPLE.COM expiring on Fri May 22 06:14:03 UTC 2020
        [Krb5LoginModule]: Entering logout
        [Krb5LoginModule]: logged out Subject
Entered SpNegoContext.acceptSecContext with state=STATE_NEW
SpNegoContext.acceptSecContext: receiving token = ... REDACTED ...
SpNegoToken NegTokenInit: reading Mechanism Oid = 1.2.840.113554.1.2.2
SpNegoToken NegTokenInit: reading Mechanism Oid = 1.2.752.43.14.3
SpNegoToken NegTokenInit: reading Mech Token
SpNegoContext.acceptSecContext: received token of type = SPNEGO NegTokenInit
SpNegoContext: negotiated mechanism = 1.2.840.113554.1.2.2
Entered Krb5Context.acceptSecContext with state=STATE_NEW
Looking for keys for: HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (1) for HTTP/a.example.com@CORP.EXAMPLE.COM
Found unsupported keytype (3) for HTTP/a.example.com@CORP.EXAMPLE.COM
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
Servlet.service() for servlet [HealthServlet] in context with path [] threw exception [Servlet execution threw an exception] with root cause
java.security.GeneralSecurityException: Checksum failed
    at java.security.jgss/sun.security.krb5.internal.crypto.dk.AesDkCrypto.decryptCTS(AesDkCrypto.java:451)
    at java.security.jgss/sun.security.krb5.internal.crypto.dk.AesDkCrypto.decrypt(AesDkCrypto.java:272)
    at java.security.jgss/sun.security.krb5.internal.crypto.Aes256.decrypt(Aes256.java:76)
    at java.security.jgss/sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType.decrypt(Aes256CtsHmacSha1EType.java:100)
    at java.security.jgss/sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType.decrypt(Aes256CtsHmacSha1EType.java:94)
    at java.security.jgss/sun.security.krb5.EncryptedData.decrypt(EncryptedData.java:180)
    at java.security.jgss/sun.security.krb5.KrbApReq.authenticate(KrbApReq.java:281)
    at java.security.jgss/sun.security.krb5.KrbApReq.<init>(KrbApReq.java:149)
    at java.security.jgss/sun.security.jgss.krb5.InitSecContextToken.<init>(InitSecContextToken.java:139)
    at java.security.jgss/sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:832)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:361)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:303)
    at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.GSS_acceptSecContext(SpNegoContext.java:905)
    at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.acceptSecContext(SpNegoContext.java:556)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:361)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:303)
    at HealthServlet.doGet(HealthServlet.kt:43)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
    at org.apache.tomcat.util.net.Nio2Endpoint$SocketProcessor.doRun(Nio2Endpoint.java:1676)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at org.apache.tomcat.util.net.AbstractEndpoint.processSocket(AbstractEndpoint.java:1087)
    at org.apache.tomcat.util.net.Nio2Endpoint$Nio2SocketWrapper$2.completed(Nio2Endpoint.java:589)
    at org.apache.tomcat.util.net.Nio2Endpoint$Nio2SocketWrapper$2.completed(Nio2Endpoint.java:567)
    at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:127)
    at java.base/sun.nio.ch.Invoker$2.run(Invoker.java:219)
    at java.base/sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:834)

Original原来的

So far the only solution I've found is to patch Samba, regressing it to the old SPN behavior.到目前为止,我发现的唯一解决方案是修补 Samba,将其回归到旧的 SPN 行为。

diff --git a/source4/heimdal/kdc/kerberos5.c b/source4/heimdal/kdc/kerberos5.c
index 27d38ad84b7..fdf249bc08d 100644
--- a/source4/heimdal/kdc/kerberos5.c
+++ b/source4/heimdal/kdc/kerberos5.c
@@ -762,9 +762,9 @@ kdc_check_flags(krb5_context context,
        return KRB5KDC_ERR_POLICY;
    }

-   if(!client->flags.client){
+   if (!is_as_req && !client->flags.client){
        kdc_log(context, config, 0,
-           "Principal may not act as client -- %s", client_name);
+           "Principal may only act as client in AS-REQ -- %s", client_name);
        return KRB5KDC_ERR_POLICY;
    }

@@ -1056,7 +1056,7 @@ _kdc_as_rep(krb5_context context,
      */

     ret = _kdc_db_fetch(context, config, client_princ,
-           HDB_F_GET_CLIENT | flags, NULL,
+           HDB_F_GET_ANY | flags, NULL,
            &clientdb, &client);
     if(ret == HDB_ERR_NOT_FOUND_HERE) {
    kdc_log(context, config, 5, "client %s does not have secrets at this KDC, need to proxy", client_name);

Edit 1编辑 1

As explained in the commit message, the behavior of using SPNs in AS-REQ is incorrect.正如提交消息中所解释的,在 AS-REQ 中使用 SPN 的行为是不正确的。

https://gitlab.com/samba-team/samba/-/commit/a6182bd9512e6c78cfd2127790419418ab776be9 https://gitlab.com/samba-team/samba/-/commit/a6182bd9512e6c78cfd2127790419418ab776be9

So the right approach would be to investigate the Checksum failed exceptions of Java instead of patching Samba.因此,正确的方法是调查 Java 的Checksum failed异常,而不是修补 Samba。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM