验证Kerberos票据的功能测试

6
我编写了一些代码,用于验证客户端在我的服务器上的Kerberos票证。我还为我的类编写了单元测试。单元测试是通过模拟调用GSS库类来编写的。但这并不足以使我充满信心,因为实际的GSS调用被模拟了。
根据我的研究,为了验证客户端的令牌,我需要使用我与KDC共享的密钥对其进行解密,我可以从keytab文件中获取该密钥。因此,为了执行验证,我需要两个东西(请纠正我):
1. 客户端的令牌 2. 服务器上的keytab文件
如果我在类路径中有这些文件,那么我是否可以执行令牌的实际验证而不需要任何模拟调用?这样做是否存在技术挑战?如果有,它们是什么?
更新1: 看起来我还需要设置一些系统属性,以便GSS库选择正确的领域、KDC等。因此,我们需要三件事:
1. Kerberos票证 2. keytab文件 3. 对应于keytab文件和票证的系统属性。
有了这个,似乎我能够得到一个端到端的测试,具有验证功能,但是只有5分钟。:)
情况是这样的,如果我拿起由KDC新生成的Kerberos令牌并将其放入我的测试中,则测试将成功运行,但在5分钟后开始失败,显示“时钟偏差太大”的异常。我更改了KDC上的Kerberos策略以生成一个永不过期的票证,但错误仍然存在。这里的好消息是,现在我有了一个概念验证,证明了这种方法的可行性。
问题归结为如何解决“时钟偏差太大”的错误。
更新2: 通过在krb.conf文件中指定它来修改时钟偏差值。那是另一个我需要设置的系统属性。有了这个,测试现在可以端到端地工作了。现在正在编写答案。
时钟偏差错误的堆栈跟踪:
Caused by: java.security.PrivilegedActionException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:422)
    at com.example.vidm.eks.request.KerberosTokenValidator.getPrincipalUserName(KerberosTokenValidator.java:91)
    at com.example.vidm.eks.request.KerberosTokenValidator.lambda$validateToken$0(KerberosTokenValidator.java:80)
    ... 7 more
Caused by: GSSException: Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))
    at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:856)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:342)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
    at sun.security.jgss.spnego.SpNegoContext.GSS_acceptSecContext(SpNegoContext.java:906)
    at sun.security.jgss.spnego.SpNegoContext.acceptSecContext(SpNegoContext.java:556)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:342)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
    at com.example.vidm.eks.krb.KerberosValidateAction.run(KerberosValidateAction.java:47)
    at com.example.vidm.eks.krb.KerberosValidateAction.run(KerberosValidateAction.java:22)
    ... 11 more
Caused by: KrbException: Clock skew too great (37)
    at sun.security.krb5.KrbApReq.authenticate(KrbApReq.java:302)
    at sun.security.krb5.KrbApReq.<init>(KrbApReq.java:149)
    at sun.security.jgss.krb5.InitSecContextToken.<init>(InitSecContextToken.java:108)
    at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:829)
    ... 19 more

我的功能测试代码:

public class KerberosTokenValidatorTest extends AbstractUnitTestBase {

  public static final String NO_PRINCIPAL = null;
  private String kerberosTicket;
  public static final String USERNAME = "username";
  private static final String REALM = "EXAMPLE.COM";
  private static final String PRINCIPAL = USERNAME + "@" + REALM;

  @BeforeClass
  public void beforeClass(){
    System.setProperty("java.security.krb5.kdc", "host/hw-99402.example.com");
    System.setProperty("java.security.krb5.realm", "EXAMPLE.COM");
    System.setProperty("javax.security.auth.useSubjectCredsOnly","false");
    String confFile = String.format("/tmp/%s", RandomStringUtils.random(10));
    try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("testkrb.conf")) {
      Files.copy(is, Paths.get(confFile));
    } catch (IOException e) {
      // An error occurred copying the resource
    }
    System.setProperty("java.security.krb5.conf", confFile);
  }

  @Test
  public void myTest() throws IOException, GSSException, ExecutionException, InterruptedException {
    KerberosTokenValidator kerberosTokenValidator = new KerberosTokenValidator();
    String kticket = FileSystemUtils.loadClasspathResourceAsString("kerberosticket");
    kerberosTokenValidator.validateToken(kticket, "hw-99402.example.com", "userPrincipalName").get();
  }

}

我的验证代码:

private String getPrincipalUserName(String token1, String serverName) throws LoginException, PrivilegedActionException {
  javax.security.auth.Subject serviceSubject = getServiceSubject(serverName);
  byte[] token = base64Decoder.decode(token1);
  KerberosTicketValidation ticketValidation = javax.security.auth.Subject.doAs(serviceSubject, new KerberosValidateAction(token));
  String kdcPrincipal = ticketValidation.getUsername();
  if (StringUtils.isBlank(kdcPrincipal)) {
    throw new LoginException("KDC principal is blank after ticket validation");
  }
  return kdcPrincipal;
}

private javax.security.auth.Subject getServiceSubject(String serverName) throws LoginException {
  String servicePrincipal = SERVICE_PRINCIPAL_SERVICE + "/" + serverName;
  final Set<Principal> princ = new HashSet<>(1);
  princ.add(new KerberosPrincipal(servicePrincipal));
  javax.security.auth.Subject sub = new javax.security.auth.Subject(false, princ, Collections.emptySet(), Collections.emptySet());
  KerberosConfig kerberosConfig = new KerberosConfig(KEYTAB_PATH, servicePrincipal);
  LoginContext lc = new LoginContext("", sub, null, kerberosConfig);
  lc.login();
  return lc.getSubject();
}

我的单元测试:

@BeforeMethod
public void setup() throws Exception {
  reset(mockGSSContext, mockGSSManager, mockGSSName);
  mockGSSManager();
}

@InjectMocks
private KerberosTokenValidator kerberosTokenValidator;

@Mock protected GSSManager mockGSSManager;
@Mock protected GSSContext mockGSSContext;
@Mock protected GSSName mockGSSName;

@Test
public void canValidateKerberosToken() throws Throwable {
  when(mockGSSName.toString()).thenReturn(PRINCIPAL);
  Subject subject = blockAndThrow(kerberosTokenValidator.validateToken(kerberosTicket, "hw-99402.vidmlabs.com", "sAMAccountName"));
  Assert.assertEquals(subject.getNameId(), USERNAME);
}

private void mockGSSManager() throws Exception {
    when(mockGSSManager.createContext((GSSCredential) null)).thenReturn(mockGSSContext);
    when(mockGSSContext.isEstablished()).thenReturn(true);
    when(mockGSSContext.acceptSecContext(any(byte[].class), anyInt(), anyInt())).thenReturn(null);
    when(mockGSSContext.getSrcName()).thenReturn(mockGSSName);
    KerberosValidateAction.setGssManager(mockGSSManager);

}

KerberosValidateAction :

public class KerberosValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
  private static GSSManager gssManager = GSSManager.getInstance();

  private byte[] kerberosTicket;
  private GSSCredential serviceCredentials;

  public KerberosValidateAction(byte[] kerberosTicket) {
    this(kerberosTicket, null);
  }

  public KerberosValidateAction(byte[] kerberosTicket, GSSCredential serviceCredentials) {
    this.kerberosTicket = kerberosTicket;
    this.serviceCredentials = serviceCredentials;
  }

  @VisibleForTesting
  public static void setGssManager(GSSManager manager) {
    gssManager = manager;
  }

  @Override
  public KerberosTicketValidation run() throws Exception {
    GSSName gssName = null;
    GSSContext context = gssManager.createContext(serviceCredentials);
    byte[] token = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
    if (!context.isEstablished()) {
      throw new ContinueNeededException(token);
    }
    gssName = context.getSrcName();
    if (gssName == null) {
      throw new AuthenticationException("GSSContext name of the context initiator is null");
    }
    context.dispose();
    return new KerberosTicketValidation(gssName.toString());
  }
}

krb5.conf文件:

[libdefaults]
    clockskew  = 999999999

你可以使用Apache目录服务器进行测试。你可以在https://github.com/apache/karaf/blob/master/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/GSSAPILdapLoginModuleTest.java找到一个典型的例子。 - Alexandre Cartapanis
嗨@AlexandreCartapanis,感谢您指出。这在实践中肯定是可行的。但问题的整个想法是避免完全设置目录。再次强调,由于我已经拥有了需要进行验证的两个实体(根据我的理解),使用Apache目录服务器将比我追求的方法过度。 - Gautam
你的代码存在逻辑错误。安全上下文是有状态的,你必须在建立之前维护它。但你没有这样做! - Michael-O
@Michael-O,你能详细解释一下吗?我真的想摆脱可能存在的任何错误。你能指点我一些阅读材料,以便我可以获得更多上下文来帮助我理解你的意思吗?提前致谢。 - Gautam
从RFC 7546开始。 - Michael-O
显示剩余2条评论
1个回答

0

执行端到端测试所需的关键组件包括:

  1. 位于认证服务器上的keytab文件
  2. 客户端从KDC获取的Kerberos票证

除此之外,服务器需要告知默认的KDC和REALM值分别对应这些keytab和令牌文件。这些可以使用系统属性来指定。

  • java.security.krb5.kdc
  • java.security.krb5.realm

这些都准备好了,使用GSS api进行身份验证的票证仍然会出现时钟偏差错误,因为Kerberos希望确保票证是在5分钟间隔内获取的。没有直接的系统属性可以设置来修改此值。但是您可以拥有一个自定义的krb5.conf文件,指定要使用该文件的系统属性并在其中放置该值。

  • java.security.krb5.conf

krb5.conf 文件:

[libdefaults]
    clockskew  = 999999999

实际上,此文件中也可以指定其他值(kdc和realm)。使用此文件还意味着必须将其写入磁盘以供库使用(尚未找到更好的方法)。

一个更直接的方法可能是利用票据生存时间而不是修改时钟偏差,但我所有的尝试都未能使用票据生存时间来覆盖时钟偏差。时钟偏差似乎完全忽略了票据生存时间。为此主题创建了另一个问题。尽管如此,对于测试时钟偏差的情况,可以省略时钟偏差覆盖,对于其他情况,可以使用大值将覆盖设置为默认测试配置。

所有要设置的属性都已在问题中更新。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接