Kerberos票证验证功能测试

时间:2019-05-26 05:59:35

标签: java kerberos gssapi

我写了一些代码来验证服务器上客户的kerberos票。我还为班级编写了单元测试。通过模拟对GSS库类的调用来编写单元测试。但是,由于模拟了实际的GSS调用,所以这并没有给我足够的信心。

到目前为止,我的研究表明,为了验证客户端令牌,我需要使用与KDC共享的密钥对它进行解密,该密钥可以从keytab文件中获得。因此,为了执行验证,我需要做两件事(需要纠正的立场):

  1. 客户的令牌
  2. 服务器上的Keytab文件

现在,如果我在类路径中有这些文件,是否可以在没有任何模拟调用的情况下对令牌进行实际验证?这样做有技术上的挑战吗?如果是,那他们是什么?

更新(第4天):

似乎我还需要设置一些系统属性,以便GSS库选择正确的领域,kdc等。因此,实质上,我们需要三件事:

  1. 一张Kerberos票
  2. 密钥表文件
  3. 与密钥表文件和票证相对应的系统属性。

有了这个,我似乎可以通过验证进行端到端的测试工作,但是只能进行5分钟。 :)

这种情况是,如果我捡起KDC新鲜生成的kerberos令牌并将其放入测试中,则测试可以成功运行,但5分钟后会开始失败,但“时钟偏斜太大”除外。我更改了KDC上的kerberos策略,以生成永不过期的凭单,但错误仍然存​​在。这里的一线希望是,现在我有了一种方法可行的概念证明。

  

问题归结为克服了“时钟偏斜太大”错误。


这是堆栈跟踪:

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");
  }

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

1 个答案:

答案 0 :(得分:0)

执行端到端测试所需的关键组件是

  1. 位于身份验证服务器上的密钥表文件
  2. 客户从KDC获得的一张kerberos票。

除了这些服务器之外,还需要告知服务器与这些密钥表和令牌文件相对应的默认KDC和REALM值是什么。可以使用系统属性

指定这些
  • 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)也可以在此文件中指定。使用此文件也将意味着必须将其写入磁盘以供库使用。 (尚未找到更好的方法)。

更直接的方法可能是使用票证生存期值而不是修改时钟偏斜,但是我所有的尝试都未能使用票证生存期来覆盖时钟偏斜。时钟偏移似乎完全忽略了票证的寿命。为此主题创建了另一个question。但是,对于测试时差情况,可以忽略时差替代,对于其他情况,可以使用较大的值将其作为默认测试配置。

该问题中所有要设置的属性均已更新。