Функциональный тест для проверки билетов Kerberos - PullRequest
5 голосов
/ 26 мая 2019

Я написал код для проверки клиентского билета Kerberos на моем сервере. Я также написал модульные тесты для своих классов. Модульные тесты написаны путем насмешек над вызовами классов библиотеки GSS. Это не вселяет в меня достаточной уверенности, поскольку фактические вызовы GSS являются поддельными.

Из моих исследований я понял, что для проверки токена клиента мне нужно будет расшифровать его с помощью общего ключа, который у меня есть с KDC, который я могу получить из файла keytab. Итак, для того, чтобы выполнить проверку, мне нужно две вещи (Подлежит исправлению):

  1. токен клиента
  2. Файл keytab на сервере

Теперь, если у меня есть эти файлы в моем classpath, могу ли я выполнить фактическую проверку токена, без каких-либо ложных вызовов? Есть ли какие-либо технические проблемы при этом? Если да, то каковы они?

Обновление 1:

Кажется, мне нужно было также установить некоторые системные свойства, чтобы библиотеки GSS выбирали правильную область, kdc и т. Д. Поэтому, по сути, нам нужно 3 вещи:

  1. Билет Kerberos
  2. Файл keytab
  3. Системные свойства, соответствующие файлу keytab и тикету.

С этим я, похоже, смогу завершить тестовую работу, с проверкой, но только в течение 5 минут. :)

Ситуация такова: если я беру токен kerberos, недавно сгенерированный KDC, и помещаю его в свой тест, тест выполняется успешно, но через 5 минут начинает проваливаться, за исключением «Слишком большой перекос часов». Я изменил политику kerberos на KDC, чтобы сгенерировать никогда не истекающий билет, но ошибка сохраняется. Серебряная подкладка здесь в том, что теперь у меня есть подтверждение концепции, что подход работает.

Проблема сводится к тому, чтобы обойти ошибку «Слишком большой перекос часов».

Обновление 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

1 Ответ

0 голосов
/ 04 июня 2019

Критическими компонентами, необходимыми для проведения сквозного теста, являются

  1. Файл keytab, расположенный на сервере аутентификации
  2. Билет Kerberos, полученный клиентом от KDC.

Помимо этого, серверу необходимо сообщить, какие значения KDC и REALM по умолчанию соответствуют этим файлам ключей и токенов. Их можно указать с помощью системных свойств

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

Они установлены, API-интерфейс GSS, используемый для аутентификации билета, все равно выдаст ошибку перекоса часов, поскольку kerberos хочет убедиться, что билет был получен в течение 5-минутного интервала. Нет прямого системного свойства, которое можно установить для изменения этого значения. Но у вас может быть собственный файл krb5.conf, укажите системное свойство для использования этого файла и поместите туда значение.

  • java.security.krb5.conf

Файл krb5.conf:

[libdefaults]
    clockskew  = 999999999

На самом деле другие значения (kdc и realm) также могут быть указаны в этом файле. Использование этого файла также будет означать, что он должен быть записан на диск, чтобы библиотека могла его найти. (Лучшего пути для этого пока не нашел).

Более простым способом могло бы стать использование значения времени жизни билета, а не изменение перекоса часов, но все мои попытки не смогли использовать время жизни билета для переопределения часового перекоса. Часовой пояс почему-то полностью игнорирует время жизни билета. Создан еще один вопрос для этой темы. Тем не менее, для тестирования сценария clockskew можно не использовать переопределение clockskew, для других сценариев переопределение можно сделать конфигурацией теста по умолчанию с использованием большого значения.

Все заданные свойства были обновлены в вопросе.

...