Tink odradza niewłaściwe praktyki związane z kluczami, takie jak:
- Dostęp użytkownika do materiału klucza tajnego – zamiast tego klucze tajne powinny być przechowywane w KMS, o ile to możliwe, za pomocą jednego z wstępnie zdefiniowanych sposobów, w jakich Tink obsługuje takie systemy.
- 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 odstępstwo od tych zasad. Tink udostępnia 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 za pomocą metody takiej jak InsecureSecretKeyAccess.get()
. W Google użytkownicy nie mogą korzystać z tej funkcji za pomocą funkcji widoczności w ramach procesu tworzenia kodu źródłowego Bazel. 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 mają żadnych materiałów tajnych, parametr secretKeyAccess
jest ignorowany.
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 zestawie kluczy i przekazuje podrzędnej funkcji podany argument secretKeyAccess
. Użytkownicy, którzy wywołują funkcję serializeKeyset
bez konieczności serializacji tajnego klucza, mogą użyć parametru null
jako drugiego argumentu. Użytkownicy, którzy chcą 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 także metadane określające sposób użycia klucza (a co za tym idzie, że nie powinien być używany w żaden inny sposób). Na przykład klucz RSA SSA PSS w Tink określa, że ten klucz RSA może być używany tylko z algorytmem podpisu PSS z określoną funkcją haszującą i określoną długością soli.
Czasami konieczne jest przekonwertowanie klucza Tink do różnych formatów, które mogą nie zawierać wszystkich tych 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 nie ma zgodności, konwersja się nie powiedzie.
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 pomogą Ci bezpiecznie pracować w takich sytuacjach.
Praktyka zalecana: weryfikacja wszystkich parametrów podczas eksportu 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 polegających na wprowadzaniu w błąd, ponieważ obiekt klucza Tink w pełni określa prawidłowy algorytm i przechowuje wszystkie metadane wraz z materiałem klucza.
Na przykład:
Niewpisanie:
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); }
Jest to podatne na błędy: w miejscu wywołania bardzo łatwo zapomnieć, że nie należy używać tego samego ecPoint
z innym algorytmem. Jeśli np. 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 na zagrożenia.
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 być natychmiast konwertowany na obiekt EcdsaPublicKey
: w tym momencie wiesz już, w jaki sposób jest używany, więc najlepiej go zadeklarować.
Powyższy kod można jeszcze ulepszyć. Zamiast przekazywania obiektu EcdsaPublicKey
lepiej jest przekazywać obiekt KeysetHandle
. Przygotowuje kod do rotacji klucza 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żywać.PublicKeyVerify
W tym momencie funkcja staje się raczej trywialna i można ją wstawić w kod.
Rekomendacja: gdy kluczowy materiał jest odczytywany z dysku lub sieci po raz pierwszy, utwórz odpowiednie obiekty Tink tak szybko, jak to możliwe.
Sposób wpisywania:
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(); }
Dzięki temu kodowi podczas odczytu tablicy bajtów od razu konwertujemy ją na obiekt Tink i określamy, który algorytm należy użyć. Takie podejście minimalizuje prawdopodobieństwo ataków polegających na mieszaniu kluczy.