Programowe eksportowanie materiału klucza

Tink odradza niewłaściwe praktyki związane z kluczami, takie jak:

  • Dostęp użytkownika do materiału klucza obiektu tajnego – w miarę możliwości klucze tajne powinny być przechowywane w KMS przy użyciu jednego ze wstępnie zdefiniowanych sposobów obsługi takich systemów przez Tink.
  • Dostęp użytkownika do części kluczy – często powoduje to błędy zgodności.

W praktyce zdarzają się jednak sytuacje, w których konieczne jest złamanie tych zasad. Tink oferuje mechanizmy, które umożliwiają bezpieczne wykonywanie tych czynności. Opisują je poniższe sekcje.

Tokeny dostępu z obiektem tajnym

Aby uzyskać dostęp do tajnego klucza, użytkownicy muszą mieć token (który jest zwykle obiektem pewnej klasy bez żadnej funkcjonalności). Token jest zwykle dostarczany przez metodę taką jak InsecureSecretKeyAccess.get(). Użytkownicy Google nie mogą używać tej funkcji, korzystając z opcji Bazel BUILD visibility. Poza Google recenzenci ds. bezpieczeństwa mogą przeszukiwać bazę kodu pod kątem użycia tej funkcji.

Jedną z przydatnych funkcji tych tokenów jest to, że można je przekazywać. Załóżmy, że masz funkcję, która serializuje dowolny klucz Tink:

String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);

W przypadku kluczy, które mają materiał klucza tajnego, ta funkcja wymaga, aby obiekt secretKeyAccess nie był pusty i miał zapisany rzeczywisty token SecretKeyAccess. W przypadku kluczy, które nie zawierają żadnych materiałów tajnych, zasada secretKeyAccess jest ignorowana.

Dzięki takiej funkcji można napisać funkcję, która serializuje cały klucz:

String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess
secretKeyAccess);

Ta funkcja wywołuje wewnętrznie funkcję serializeKey dla każdego klucza w zbiorze kluczy i przekazuje podrzędną funkcję podany argument secretKeyAccess. Użytkownicy, którzy następnie wywołają serializeKeyset bez konieczności serializowania materiału klucza obiektu tajnego, mogą używać null jako drugiego argumentu. Użytkownicy, którzy muszą serializować materiały klucza tajnego, powinni użyć InsecureSecretKeyAccess.get().

Dostęp do części klucza

Klucze Tink zawierają nie tylko surowy materiał klucza, ale też metadane określające, jak należy go używać (i co za tym idzie, jak nie należy). Na przykład klucz RSA SSA PSS w Tink wskazuje, że tego klucza RSA można używać tylko z algorytmem podpisu PSS z użyciem określonej funkcji skrótu i określonej długości ciągu zaburzającego.

Czasami konieczne jest przekonwertowanie klucza Tink na różne formaty, które mogą nie określać wszystkich metadanych. Oznacza to zwykle, że podczas używania klucza należy podać metadane. Inaczej mówiąc, zakładając, że klucz jest zawsze używany z tym samym algorytmem, taki klucz nadal pośrednio zawiera te same metadane, ale są one przechowywane w innym miejscu.

Podczas konwertowania klucza Tink na inny format musisz się upewnić, że metadane klucza Tink są zgodne z metadanymi (określonymi domyślnie) innego formatu klucza. Jeśli dane nie są zgodne, konwersja musi się nie udać.

Ponieważ te kontrole są często niepełne lub nieobecne, Tink ogranicza dostęp do interfejsów API, które dają dostęp do klucza, który jest tylko częściowy, ale może zostać uznany za pełny. W Javie Tink używa interfejsu RestrictedApi, a w C++ i Golang używa tokenów podobnych do tokenów dostępu do tajnego klucza.

Użytkownicy tych interfejsów API są odpowiedzialni za zapobieganie atakom polegającym na ponownym użyciu klucza oraz niespójnościom.

Najczęściej spotykane są metody, które ograniczają „częściowy dostęp do klucza” w kontekście eksportowania kluczy z Tink lub importowania ich do Tink. Poniżej znajdziesz sprawdzone metody, które wyjaśniają, jak bezpiecznie wykonywać działania w takich sytuacjach.

Praktyka: weryfikuj wszystkie parametry podczas eksportowania klucza

Jeśli na przykład napiszesz funkcję, która eksportuje klucz publiczny HPKE:

Nieprawidłowy sposób eksportowania klucza publicznego:

/** Provide the key to our users which don't have Tink. */
byte[] exportTinkHpkeKey(HpkePublicKey key) {
    return key.getPublicKeyBytes().toByteArray();
}

To jest problem. Po otrzymaniu klucza strona trzecia, która go używa, dokonuje pewnych założeń dotyczących jego parametrów: na przykład zakłada, że algorytm HPKE AEAD użyty do tego 256-bitowego klucza to AES-GCM.

Rekomendacja: sprawdź, czy parametry są zgodne z oczekiwaniami w przypadku eksportu kluczy.

Lepszy sposób eksportowania klucza publicznego:

/** Provide the key to our users which don't have Tink. */
byte[] exportTinkHpkeKeyForOurUsers(HpkePublicKey key) {
    // Our users assume we use KEM_P256_HKDF_SHA256 for the KEM.
    if (!key.getParameters().getKemId().equals(HpkeParameters.KemId.KEM_P256_HKDF_SHA256)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    // Our users assume we use HKDF SHA256 to create the key material.
    if (!key.getParameters().getKdfId().equals(HpkeParameters.KdfId.HKDF_SHA256)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    // Our users assume that we use AES GCM with 256 bit keys.
    if (!key.getParameters().getAeadId().equals(HpkeParameters.AeadId.AES_256_GCM)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    // Our users assume we follow the standard and don't add a Tink style prefix
    if (!key.getParameters().getVariant().equals(HpkeParameters.Variant.NO_PREFIX)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    return key.getPublicKeyBytes().toByteArray();
}

Sprawdzona metoda: używaj obiektów Tink jak najwcześniej podczas importowania kluczy

Pozwala to zminimalizować ryzyko ataków związanych z dezorientacją klawiszy, ponieważ obiekt Tink Key w pełni określa właściwy algorytm i przechowuje wszystkie metadane wraz z materiałem klucza.

Na przykład:

Użycie inne niż wpisane:

void verifyEcdsaSignature(ECPoint ecPoint, byte[] signature, byte[] message)
        throws Exception {
    EcdsaParameters parameters =
        EcdsaParameters.builder()
            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
            .setHashType(EcdsaParameters.HashType.SHA256)
            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
            .build();
    EcdsaPublicKey key =
        EcdsaPublicKey.builder()
            .setParameters(parameters)
            .setPublicPoint(ecPoint)
            .build();
    KeysetHandle handle = KeysetHandle.newBuilder()
       .addEntry(KeysetHandle.importKey(key).withFixedId(1).makePrimary())
       .build();
    PublicKeyVerify publicKeyVerify = handle.getPrimitive(RegistryConfiguration.get(), PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

To sprzyja popełnianiu błędów: w miejscu wywołania bardzo łatwo zapomnieć, że nie należy używać tego samego parametru ecPoint z innym algorytmem. Jeśli na przykład istnieje podobna funkcja o nazwie encryptWithECHybridEncrypt, wywołujący może użyć tego samego punktu krzywej do zaszyfrowania wiadomości, co może łatwo prowadzić do podatności.

Zamiast tego lepiej zmienić wartość verifyEcdsaSignature, tak aby pierwszy argument miał wartość EcdsaPublicKey. W rzeczywistości za każdym razem, gdy klucz jest odczytywany z dysku lub sieci, powinien zostać od razu skonwertowany na obiekt EcdsaPublicKey: na tym etapie wiesz już, w jaki sposób jest używany klucz, więc najlepiej to zrobić.

Powyższy kod można jeszcze ulepszyć. Zamiast EcdsaPublicKey, lepiej jest podać KeysetHandle. Przygotowuje on kod do rotacji kluczy bez konieczności wykonywania dodatkowych czynności. Dlatego to rozwiązanie jest preferowane.

Nie kończymy jednak na ulepszeniach: jeszcze lepszym rozwiązaniem jest przekazanie obiektu PublicKeyVerify: wystarczy on do wykonania tej funkcji, a jego przekazanie może zwiększyć liczbę miejsc, w których można jej użyć. W tym momencie funkcja staje się raczej trywialna i można ją wstawić w kod.

Zalecenie: przy pierwszym odczytywaniu materiału klucza z dysku lub sieci jak najszybciej utwórz odpowiednie obiekty Tink.

Sposób użycia:

KeysetHandle readEcdsaKeyFromFile(Path fileWithEcdsaKey) throws Exception {
    byte[] content = Files.readAllBytes(fileWithEcdsaKey);
    BigInteger x = new BigInteger(1, Arrays.copyOfRange(content, 0, 32));
    BigInteger y = new BigInteger(1, Arrays.copyOfRange(content, 32, 64));
    ECPoint point = new ECPoint(x, y);
    EcdsaParameters parameters =
        EcdsaParameters.builder()
            .setSignatureEncoding(EcdsaParameters.SignatureEncoding.IEEE_P1363)
            .setCurveType(EcdsaParameters.CurveType.NIST_P256)
            .setHashType(EcdsaParameters.HashType.SHA256)
            .setVariant(EcdsaParameters.Variant.NO_PREFIX)
            .build();
    EcdsaPublicKey key =
        EcdsaPublicKey.builder()
            .setParameters(parameters)
            .setPublicPoint(ecPoint)
            .build();
    return KeysetHandle.newBuilder()
       .addEntry(KeysetHandle.importKey(key).withFixedId(1).makePrimary())
       .build();
}

Za pomocą takiego kodu od razu konwertujemy tablicę bajtów na obiekt Tink po jego odczytaniu i w pełni określamy, jaki algorytm ma zostać użyty. Takie podejście minimalizuje prawdopodobieństwo ataków polegających na zaciemnieniu klucza.