Kontrola dostępu

Jednym z celów Tink jest zniechęcanie do stosowania złych praktyk. W tej sekcji szczególnie ważne są 2 punkty:

  1. Tink zachęca do użytkowania w taki sposób, że użytkownicy nie mają dostępu do materiału klucza obiektu tajnego. Klucze tajne należy w miarę możliwości przechowywać w KMS przy użyciu jednego ze wstępnie zdefiniowanych sposobów, w jakie Tink obsługuje takie systemy.
  2. Tink zniechęca użytkowników do uzyskiwania dostępu do części kluczy, ponieważ często powoduje to błędy zgodności.

W praktyce oczywiście czasami trzeba naruszać obie te zasady. W tym celu Tink udostępnia różne mechanizmy.

Tajne tokeny dostępu do klucza

Aby uzyskać dostęp do materiału klucza obiektu tajnego, użytkownicy muszą mieć token (zwykle jest to obiekt jakiejś klasy bez żadnych funkcji). Token jest zwykle dostarczany za pomocą metody takiej jak InsecureSecretKeyAccess.get(). W Google użytkownicy nie mogą używać tej funkcji za pomocą ustawienia widoczności w usłudze Bazel BUILD. Poza Google weryfikatorzy zabezpieczeń mogą wyszukiwać w bazie kodu przypadki użycia tej funkcji.

Jedną z przydatnych funkcji takich tokenów jest możliwość ich przekazywania. Załóżmy na przykład, że masz funkcję, która serializuje dowolny klucz Tink:

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

W przypadku kluczy, które zawierają materiał klucza obiektu tajnego, funkcja ta wymaga, aby obiekt secretKeyAccess nie miał wartości null i był przechowywany rzeczywisty token SecretKeyAccess. W przypadku kluczy, które nie mają żadnego materiału tajnego, secretKeyAccess jest ignorowana.

Biorąc pod uwagę taką funkcję, można napisać funkcję, która zserializuje cały zestaw kluczy: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccesssecretKeyAccess);

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

Dostęp do części klucza

Względnie często występujący błąd w zabezpieczeniach to „atak wykorzystujący ponowne użycie klucza”. Ten błąd może wystąpić, gdy użytkownicy ponownie używają na przykład modułu n oraz wykładników d i e klucza RSA w 2 różnych ustawieniach (np. do obliczania podpisów i szyfrowania)1.

Innym stosunkowo częstym błędem w przypadku kluczy kryptograficznych jest wskazanie części klucza, a następnie „przejęcie” metadanych. Załóżmy na przykład, że użytkownik chce wyeksportować klucz publiczny RSASSA-PSS z Tink, aby użyć go z inną biblioteką. W Tink te klucze składają się z tych części:

  • Moduł sprężystości n
  • Wykładnik publiczny e
  • Specyfikacja dwóch funkcji skrótu używanych wewnętrznie
  • Długość soli używana wewnętrznie w algorytmie.

Podczas eksportowania takiego klucza możesz zignorować funkcje skrótu i ciąg zaburzający. Często się to sprawdza, ponieważ inne biblioteki nie wymagają funkcji haszowania (i na przykład przyjmują, że używany jest SHA256), a funkcja skrótu używana w Tink jest przypadkowo taka sama jak w innej bibliotece (lub funkcje haszujące zostały wybrane specjalnie, aby działała razem z inną biblioteką).

Jednak ignorowanie funkcji haszowania może być potencjalnie kosztownym błędem. Aby to zobaczyć, załóżmy, że później do zestawu kluczy Tink został dodany nowy klucz z inną funkcją haszowania. Załóżmy, że klucz jest eksportowany za pomocą tej metody i przekazywany partnerowi biznesowemu, który używa go z inną biblioteką. Tink przyjmuje teraz inną wewnętrzną funkcję skrótu i nie może zweryfikować podpisu.

W takim przypadku funkcja eksportująca klucz powinna zakończyć się niepowodzeniem, jeśli funkcja skrótu nie będzie zgodna z oczekiwaniami innej biblioteki. W przeciwnym razie wyeksportowany klucz jest bezużyteczny, ponieważ tworzy niezgodne teksty zaszyfrowane lub podpisy.

Aby zapobiec takim pomyłkom, Tink ogranicza funkcje przyznające dostęp do materiału klucza, który jest tylko częściowy, ale może zostać pomylony jako pełny klucz. Na przykład w Javie Tink używa do tego celu RestrictedApi.

Gdy użytkownik korzysta z takiej adnotacji, odpowiada za zapobieganie atakom związanym z ponownym użyciem klucza i niezgodnością.

Sprawdzona metoda: używaj obiektów Tink jak najszybciej podczas importowania klucza

Podczas eksportowania kluczy z Tink lub ich importowania do nich najczęściej napotykasz metody, które są ograniczone przez „dostęp częściowy dostępu do klucza”.

Zmniejsza to ryzyko ataków pomylących, ponieważ obiekt klucza Tink w pełni określa prawidłowy algorytm i przechowuje wszystkie metadane razem z materiałem klucza.

Na przykład:

Użycie bez typu:

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(PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

Jest to podatne na błędy: w witrynie rozmowy telefonicznej bardzo łatwo zapomnieć, że nigdy nie należy używać takiego samego elementu ecPoint z innym algorytmem. Jeśli na przykład istnieje podobna funkcja o nazwie encryptWithECHybridEncrypt, element wywołujący może użyć tego samego punktu krzywej do zaszyfrowania wiadomości, co łatwo może doprowadzić do powstania luk w zabezpieczeniach.

Zamiast tego lepiej jest zmienić właściwość verifyEcdsaSignature, aby pierwszy argument miał wartość EcdsaPublicKey. Za każdym razem, gdy klucz jest odczytywany z dysku lub sieci, powinien zostać natychmiast przekonwertowany na obiekt EcdsaPublicKey: w tym momencie wiesz już, w jaki sposób jest on używany, dlatego najlepiej jest go przyjąć.

Poprzedni kod można jeszcze bardziej ulepszyć. Zamiast przekazywać EcdsaPublicKey, lepiej jest przekazać właściwość KeysetHandle. Przygotowuje kod do rotacji kluczy bez konieczności wykonywania dodatkowych czynności. Dlatego to rozwiązanie powinno być preferowane.

Nie wprowadziliśmy jednak ulepszeń: jeszcze lepiej jest przekazać obiekt PublicKeyVerify – wystarcza to w przypadku tej funkcji, więc przekazanie obiektu PublicKeyVerify może zwiększyć liczbę miejsc, w których można jej używać. W tym momencie funkcja staje się jednak prosta i można ją uzupełnić.

Zalecenie: gdy materiał klucza jest odczytywany po raz pierwszy z dysku lub sieci, jak najszybciej utwórz odpowiednie obiekty Tink.

Wpisane użycie:

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

Przy użyciu takiego kodu natychmiast konwertujemy tablicę bajtów na obiekt Tink, gdy jest ona odczytywana, i w pełni określamy, jakiego algorytmu należy użyć. To podejście minimalizuje ryzyko ataków polegających na dezorientacji.

Sprawdzona metoda: sprawdź wszystkie parametry w eksporcie klucza

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

Niewłaściwy sposób wyeksportowania klucza publicznego:

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

Jest to problematyczne. Po otrzymaniu klucza używana przez niego firma zewnętrzna przyjmuje pewne założenia dotyczące jego parametrów: np. przyjmuje, że algorytm HPKE AEAD używany dla tego klucza o długości 256 bitów to AES-GCM itd.

Zalecenie: sprawdź, czy parametry są zgodne z oczekiwanymi podczas eksportowania kluczy.

Lepszy sposób eksportowania klucza publicznego:

/** Provide the key to our users which do not 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 do not add a Tink style prefix
    if (!key.getParameters().getVariant().equals(HpkeParameters.Variant.NO_PREFIX)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    return key.getPublicKeyBytes().toByteArray();
}