Функциональный тест для проверки билетов Kerberos

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

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

  1. Токен клиента
  2. Keytab файл на сервере

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

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

person Gautam    schedule 26.05.2019    source источник
comment
Вы можете использовать сервер каталогов apache для выполнения теста. Вы можете найти типичный пример по адресу github.com/apache/karaf/blob/master/jaas/modules/src/test/java/   -  person Alexandre Cartapanis    schedule 26.05.2019
comment
Привет @AlexandreCartapanis, спасибо, что указали. Это то, что определенно может работать на практике. Но вся идея вопроса заключалась в том, чтобы вообще не настраивать каталог. Опять же, поскольку у меня уже есть два объекта, которые мне нужны для проверки (насколько я понимаю), использование сервера каталогов Apache будет излишним по сравнению с подходом, который я использую.   -  person Gautam    schedule 26.05.2019
comment
У вас логическая ошибка в коде. Контекст безопасности имеет состояние, вы должны поддерживать его, пока он не будет установлен. Вы не!   -  person Michael-O    schedule 31.05.2019
comment
@ Майкл-О, не могли бы вы немного уточнить? Я действительно хотел бы избавиться от любых ошибок, которые могут быть там. Есть ли что-нибудь для чтения, на которое вы можете мне указать, чтобы я мог получить больше контекста, чтобы помочь мне понять, что вы говорите? Заранее спасибо.   -  person Gautam    schedule 31.05.2019
comment
Начните с RFC 7546.   -  person Michael-O    schedule 01.06.2019
comment
@ Майкл-О, я кое-что прочитал и даже обсудил со своими коллегами. Это не очень ясно. Не могли бы вы указать, о какой линии вы говорите?   -  person Gautam    schedule 13.06.2019
comment
context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length); Как только вы снова вводите run(), вы создаете новый контекст. Это не правильно. Вы должны сохранить его. В лучшем случае используйте PriviledgedAction только для получения учетных данных сервера.   -  person Michael-O    schedule 13.06.2019


Ответы (1)


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

  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 каким-то образом полностью игнорирует время жизни билета. Создал еще один вопрос для этой темы. Тем не менее, для тестирования сценария перекоса часов можно опустить переопределение перекоса часов, для других сценариев переопределение можно сделать тестовой конфигурацией по умолчанию, используя большое значение.

Все свойства, которые нужно установить, были обновлены в вопросе.

person Gautam    schedule 04.06.2019