【问题标题】:Kerberos SPNEGO Authentication with multiple SPN and AES256 on Samba 4.11在 Samba 4.11 上使用多个 SPN 和 AES256 进行 Kerberos SPNEGO 身份验证
【发布时间】:2023-11-08 12:54:02
【问题描述】:

我有一个用 Kotlin 编写的 HTTP 服务,并使用 Tomcat 来侦听多个域,并且这些域需要通过 Kerberos 进行身份验证。在 Samba 4.9 上,我们有一个用户拥有多个启用了 AES256 加密的 SPN。为该用户生成了一个包含所有 SPN 的密钥表。

升级到 Samba 4.11 后,单个用户中的多个 SPN 停止工作。抛出了错误Client 'HTTP/a.example.com@CORP.EXAMPLE.COM' not found in Kerberos database while getting initial credentials。我们通过创建多个用户来解决此问题,每个 SPN 一个用户并将 UPN 设置为单个 SPN 的值。之后,我们为每个用户生成了 keytab,然后我们将其合并。

问题是,当我收到一张带有aes256-cts-hmac-sha1-96 的票时,java.security.GeneralSecurityException: Checksum failed 被抛出,并且只在一个域中工作,即我用作主体的域。 arcfour-hmac-md5 在所有域上都可以正常工作,但我需要支持 AES 加密。

我已经在我们的旧 Samba 4.9 上测试了这个场景,同样的情况也发生了。如果我们有多个用户,每个用户都有一个 SPN,并且所有用户都有一个 keytab,那么Checksum failed 也会被抛出。

因此,要么我设法让一个具有多个 SPN 的用户在 Samba 4.11 上工作,要么在使用 AES 加密时我必须摆脱Checksum failed

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

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

.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

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

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()
    }
}

日志

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)

【问题讨论】:

    标签: java kerberos samba spnego


    【解决方案1】:

    原创

    到目前为止,我找到的唯一解决方案是修补 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);
    

    编辑 1

    正如提交消息中所解释的,在 AS-REQ 中使用 SPN 的行为是不正确的。

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

    因此,正确的方法是调查 Java 的 Checksum failed 异常,而不是修补 Samba。

    【讨论】:

      最近更新 更多