Kerberos票证验证功能测试

Gau*_*tam 5 java kerberos gssapi

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

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

  1. 客户的代币
  2. 服务器上的Keytab文件

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

更新1:

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

  1. 一张非洲球票
  2. 密钥表文件
  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
Run Code Online (Sandbox Code Playgroud)

我的功能测试代码:

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

}
Run Code Online (Sandbox Code Playgroud)

我的验证码:

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();
}
Run Code Online (Sandbox Code Playgroud)

我的单元测试:

@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);
Run Code Online (Sandbox Code Playgroud)

}

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());
  }
}
Run Code Online (Sandbox Code Playgroud)

krb5.conf文件:

[libdefaults]
    clockskew  = 999999999
Run Code Online (Sandbox Code Playgroud)

Gau*_*tam 0

The critical components needed to perform an end to end test are

  1. A keytab file located on the authenticating server
  2. A kerberos ticket that the client has obtained from KDC.

Apart from these the server needs to be told what the default KDC and REALM values are that correspond to these keytab and token files. These can be specified using the system properties

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

These in place, GSS api used to authenticate the ticket will still give a clock skew error as kerberos wants to make sure that the ticket was obtained within a 5 minutes interval. There is no direct system property that can be set to modify this value. But you can have a custom krb5.conf file, specify the system property to use this file and put the value there.

  • java.security.krb5.conf

krb5.conf file :

[libdefaults]
    clockskew  = 999999999
Run Code Online (Sandbox Code Playgroud)

In fact the other values ( kdc and realm ) can also be specified in this file. Using this file will also mean that it will have to be written on disk for the library to pick it up. ( Haven't found a better way for it yet ).

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

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