JAAS - 无法将 Kerberos 票证保存到缓存文件,并且无法从头开始创建缓存......和其他细节

JAAS - fails to persist Kerberos ticket to cache file, and unable to create cache from scratch.. and other details

我正在开发一个使用 JAAS 执行身份验证的 Java 应用程序,应该按如下方式工作:(i) 当用户 uclient 的票证已经在本地缓存中时,它应该对用户进行身份验证不询问凭据,(ii) 当缓存中没有 'uclient' 的票证时,它应该询问 username/password 并将获得的票证保存到本地缓存中。

我的应用程序能够执行 'i' 但无法执行 'ii',它正确验证了用户(创建 Subject/Principal)但它不会保留 Krb票入缓存。

问题

  1. 我如何achieve/implement这个?
  2. 而且..这会创建 empty/nonexistent 时的 Kerberos 缓存文件? - 如何以编程方式从 Java 实现缓存文件 creation/initialization?
  3. 而且..只是为了 好奇,Java JaaS 是否能够管理 linux KEYRING? (在 Jaas 无法自动管理它们的那一刻)
  4. Java JaaS 是否只能 manage/persist 缓存中默认主体的票证? - 或者我如何使用 JaaS 管理我在单个缓存文件中有很多委托人的票证的情况?

请注意,我的应用程序必须在 Windows AD 和 Linux 领域中工作。

有关我的环境和当前代码的更多数据

我正在 Linux Kerberos 领域中测试客户端应用程序,该领域配置了 FreeIPA 客户端和服务器。我有一个为领域 AUTHDEMO.IT 提供 KDC 的 Linux VM,以及一个被认可到 AUTHDEMO.IT 领域的 linux VM。 krb5.conf配置:

includedir /var/lib/sss/pubconf/krb5.include.d/

[libdefaults]
  default_realm = AUTHDEMO.IT
  dns_lookup_realm = true
  dns_lookup_kdc = true
  rdns = false
  ticket_lifetime = 24h
  forwardable = true
  udp_preference_limit = 0
  default_ccache_name = KEYRING:persistent:%{uid}


[realms]
  AUTHDEMO.IT = {
    pkinit_anchors = FILE:/etc/ipa/ca.crt

  }


[domain_realm]
  .authdemo.it = AUTHDEMO.IT
  authdemo.it = AUTHDEMO.IT

这是 jaas.conf 的应用程序:

JaasDemo {
   com.sun.security.auth.module.Krb5LoginModule required 
   useTicketCache=true
   principal=uclient
   debug=true; 
};

我没有指定默认缓存文件名,我在调试中验证它默认为:/tmp/krb5cc_1000 其中 1000 是 运行 用户的 uid。

在 JaasDemo class 实例中,我使用此 login 方法执行身份验证:

public LoginContext login(){
        LoginContext lc = null;
        try {
            System.out.println("Initialize logincontext");
            lc = new LoginContext("JaasLogin",
                    new TextCallbackHandler());
        } catch (LoginException | SecurityException le) {
            System.err.println("Cannot create LoginContext."
                    + le.getMessage());
            return lc;
        }

        try {
            // attempt authentication
            System.out.println("Attempt login");
            lc.login();
        } catch (LoginException le) {
            System.err.println("Authentication failed:");
            System.err.println("  " + le.getMessage());
            return lc;
        }

        System.out.println("Authentication succeeded!");
        return lc;
    }

我已经使用此命令执行了我的应用程序(注意详细 kerberos 日志记录的选项):

java  -Dsun.security.krb5.debug=true -Dsun.security.jgss.debug=true -Djava.security.auth.login.config=jaas.conf -jar myapp.jar

以下是应用程序在不同情况下的输出,请注意,当被询问时,用户会以交互方式提供正确的凭据。 第一种情况不存在/tmp/krb5cc_1000文件:

Initialize logincontext
Attempt login
Debug is  true storeKey false useTicketCache true useKeyTab false doNotPrompt false ticketCache is null isInitiator true KeyTab is null refreshKrb5Config is false principal is uclient tryFirstPass is false useFirstPass is false storePass is false clearPass is false
Acquire TGT from Cache
>>>KinitOptions cache name is /tmp/krb5cc_1000
Principal is uclient@AUTHDEMO.IT
null credentials from Ticket Cache
**Login Handler invoked, providing username and password to login manager..**
        [Krb5LoginModule] user entered username: uclient

Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23.
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=authdemo2.authdemo.it. UDP:88, timeout=30000, number of retries =3, #bytes=143
>>> KDCCommunication: kdc=authdemo2.authdemo.it. UDP:88, timeout=30000,Attempt =1, #bytes=143
>>> KrbKdcReq send: #bytes read=283
>>>Pre-Authentication Data:
     PA-DATA type = 136

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = REMOVED 3@, s2kparams = null
     PA-ETYPE-INFO2 etype = 17, salt = REMOVED, s2kparams = null

>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 133

>>> KdcAccessibility: remove authdemo2.authdemo.it.:88
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
     cTime is Wed Jun 29 17:12:49 CEST 1988 583600369000
     sTime is Wed Aug 02 15:53:28 CEST 2017 1501682008000
     suSec is 981130
     error code is 25
     error Message is Additional pre-authentication required
     cname is uclient@AUTHDEMO.IT
     sname is krbtgt/AUTHDEMO.IT@AUTHDEMO.IT
     eData provided.
     msgType is 30
>>>Pre-Authentication Data:
     PA-DATA type = 136

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = REMOVED 3@, s2kparams = null
     PA-ETYPE-INFO2 etype = 17, salt = REMOVED, s2kparams = null

>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 133

KRBError received: NEEDED_PREAUTH
KrbAsReqBuilder: PREAUTH FAILED/REQ, re-send AS-REQ
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23.
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23.
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=authdemo2.authdemo.it. UDP:88, timeout=30000, number of retries =3, #bytes=225
>>> KDCCommunication: kdc=authdemo2.authdemo.it. UDP:88, timeout=30000,Attempt =1, #bytes=225
>>> KrbKdcReq send: #bytes read=674
>>> KdcAccessibility: remove authdemo2.authdemo.it.:88
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsRep cons in KrbAsReq.getReply uclient
principal is uclient@AUTHDEMO.IT
Commit Succeeded 

Authentication succeeded!


Subject.toString:
    Principal: uclient@AUTHDEMO.IT
    Private Credential: Ticket (hex) = 
REMOVED TICKET DETAILS                                             K.

Client Principal = uclient@AUTHDEMO.IT
Server Principal = krbtgt/AUTHDEMO.IT@AUTHDEMO.IT
Session Key = EncryptionKey: keyType=18 keyBytes (hex dump)=
REMOVED

Forwardable Ticket true
Forwarded Ticket false
Proxiable Ticket false
Proxy Ticket false
Postdated Ticket false
Renewable Ticket false
Initial Ticket false
Auth Time = Wed Aug 02 15:53:28 CEST 2017
Start Time = Wed Aug 02 15:53:28 CEST 2017
End Time = Thu Aug 03 15:53:28 CEST 2017
Renew Till = null
Client Addresses  Null 

第二种情况 /tmp/krb5cc_1000 文件存在,其中包含另一个用户的票证(使用 kinit -c 创建);应用程序正确验证,但获取的票证不会保存到缓存文件中。

klist 状态优先于应用程序执行:

klist -c /tmp/krb5cc_1000 

Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: otheruser@AUTHDEMO.IT

Valid starting       Expires              Service principal
08/02/2017 16:05:19  08/03/2017 16:05:13  krbtgt/AUTHDEMO.IT@AUTHDEMO.IT

应用程序输出:

Initialize logincontext
Attempt login
Debug is  true storeKey false useTicketCache true useKeyTab false doNotPrompt false ticketCache is null isInitiator true KeyTab is null refreshKrb5Config is false principal is uclient tryFirstPass is false useFirstPass is false storePass is false clearPass is false
Acquire TGT from Cache
>>>KinitOptions cache name is /tmp/krb5cc_1000
java.io.IOException: Primary principals don't match.
    at sun.security.krb5.internal.ccache.FileCredentialsCache.load(FileCredentialsCache.java:179)
    at sun.security.krb5.internal.ccache.FileCredentialsCache.acquireInstance(FileCredentialsCache.java:82)
    at sun.security.krb5.internal.ccache.CredentialsCache.getInstance(CredentialsCache.java:83)
    at sun.security.krb5.Credentials.acquireTGTFromCache(Credentials.java:333)
    at com.sun.security.auth.module.Krb5LoginModule.attemptAuthentication(Krb5LoginModule.java:665)
    at com.sun.security.auth.module.Krb5LoginModule.login(Krb5LoginModule.java:617)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at javax.security.auth.login.LoginContext.invoke(LoginContext.java:755)
    at javax.security.auth.login.LoginContext.access[=16=]0(LoginContext.java:195)
    at javax.security.auth.login.LoginContext.run(LoginContext.java:682)
    at javax.security.auth.login.LoginContext.run(LoginContext.java:680)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.login.LoginContext.invokePriv(LoginContext.java:680)
    at javax.security.auth.login.LoginContext.login(LoginContext.java:587)
    at it.kerberosdemo.login.JaasDemo.login(JaasDemo.java:45)
    at it.kerberosdemo.login.JaasDemo.login(JaasDemo.java:27)
    at it.male.kerberosdemo.client.ClientMain.main(ClientMain.java:29)
Principal is uclient@AUTHDEMO.IT
null credentials from Ticket Cache
Login Handler invokerd, providing username and password to login manager..
        [Krb5LoginModule] user entered username: uclient

Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23.
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=authdemo2.authdemo.it. UDP:88, timeout=30000, number of retries =3, #bytes=143
>>> KDCCommunication: kdc=authdemo2.authdemo.it. UDP:88, timeout=30000,Attempt =1, #bytes=143
>>> KrbKdcReq send: #bytes read=283
>>>Pre-Authentication Data:
     PA-DATA type = 136

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = REMOVED, s2kparams = null
     PA-ETYPE-INFO2 etype = 17, salt = REMOVED, s2kparams = null

>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 133

>>> KdcAccessibility: remove authdemo2.authdemo.it.:88
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
     cTime is Mon Sep 22 16:38:56 CEST 2031 1947854336000
     sTime is Wed Aug 02 16:07:05 CEST 2017 1501682825000
     suSec is 803283
     error code is 25
     error Message is Additional pre-authentication required
     cname is uclient@AUTHDEMO.IT
     sname is krbtgt/AUTHDEMO.IT@AUTHDEMO.IT
     eData provided.
     msgType is 30
>>>Pre-Authentication Data:
     PA-DATA type = 136
...OMITTED IDENTICAL

klist 确认没有将 'uclient' 的票添加到缓存文件中。

此致

你不能。 Java 不支持将您的 TGT 或服务票证保存回可与 MIT Kerberos 或 Heimdal 一起使用的基于文件的缓存。 Oracle 有一些私有 类 可以执行此操作,但我不建议这样做。

终于找到问题1+2的答案了

与 java 发行版捆绑在一起的 kinit 命令是一个 java 应用程序,用于验证用户进入 realm/domain 并将获取的票证保存在 ccache 文件。 kinit 命令代码在 OpenJDK 的 sun.security.krb5.internal.tools 包中可用。 主要class是sun.security.krb5.internal.tools.Kinit。为了获取(验证)并保留 Kerberos 票据,您可以将所有 tool 包复制到您的应用程序中,并通过提供 cli 从 Kinit class 调用方法 main(String[] arv)争论。您也可以像我一样更改 Kinit class 以便更好地与您的代码集成。

Kinit 代码对于了解内部私有 Kerberos 代码的内部工作原理和自定义它非常有用。例如,有一个 KDCOptions 实例,您可以手动配置它以请求可更新票证等等。让我们研究一下吧! ;-)

请考虑:

  • 不保证将来JDK 版本
  • 内部代码接口保持不变
  • 不保证不同 JDK 供应商之间的内部代码接口相同。

我可以确认我的代码在 OpenJDK 和 Oracle JDK 上都能正常工作。

大局

目前,我的应用程序使用 Jaas 来通过查看本地 ccache 文件中的 Krb 凭据进行身份验证,如果失败,它会执行上述 kinit 代码。然后,它使用更新后的 ccache 文件中的 Jaas 进行身份验证。

下一步

我目前正在尝试将 Kerberos 票证持久化以直接从主题对象中的凭据缓存。
我会尝试使用 sun.security.krb5.internal.ccache.FileCredentialCache class 但它看起来是一种低级方法。 看看kinit代码中CredentialCache抽象class的使用,可能会有用。 如果成功,我会更新线程。

谢谢

感谢 Michael-O 向我展示了 sun.security.krb5.internal 软件包,我终于在其中找到了 kinit 代码。

此致

其他问题

3 - 出于好奇,Java JaaS 是否能够管理 linux KEYRING? (目前 Jaas 无法自动管理它们)

没有, 内部 Java Krb 类 只管理文件而不是 KEYRING。

4 - Java JaaS 是否只能 manage/persist 缓存中默认主体的票证? - 或者我如何使用 JaaS 管理我在单个缓存文件中有很多委托人的票证的情况?

我没有发现管理集合的简单方法(这是最近的标准),我个人的选择是为每个主体创建一个缓存文件。

不幸的是,许多 Java 开发人员不了解 MIT Kerberos 的底层,他们默认导入内置本机库。 所以您不能使用 Java JAAS,因为它不支持 Linux 和 Windows Kerberos。 Credential Cache 是 MIT 的强大标准,它是 HA 和身份验证优化的基础,因为当用户或 web-gate 服务已经通过身份验证并在生命周期限制内连接到目标服务时,SSO 不需要重复身份验证。

自协议出现以来,Oracle 和 OpenJDK 为 MIT 和 Heimdal 提供了 Sun GSSAPI 类。

这是更改身份验证低级算法的最简单方法,您只应在 JAVA_OPTS 或 java 参数中添加: -Dsun.security.jgss.native=true -Djavax.security.auth.useSubjectCredsOnly=false

因此此解决方案适用于任何 OS 并有助于将 TGT 和 TGS 存储到用户凭据缓存文件。 对于将使用本机 jgss 的目标服务,您必须通过执行“export KRB5_KTNAME=/path-to-keytab”添加到环境 SPN keytab 以解密用户 TGT。 出于安全原因,还应将凭据缓存文件保存到客户端或服务器应用程序所有者的主目录中,因为默认 /tmp/krb5cc_%uid% 有时不是个好主意。