Zugriffskontrolle

Ein Ziel von Tink ist es, schlechten Praktiken entgegenzuwirken. Von besonderem Interesse sind in diesem Abschnitt zwei Punkte:

  1. Tink fördert die Nutzung so, dass Nutzer nicht auf geheimes Schlüsselmaterial zugreifen können. Stattdessen sollten geheime Schlüssel nach Möglichkeit in einem KMS gespeichert werden. Verwenden Sie dazu eine der vordefinierten Methoden, mit denen Tink solche Systeme unterstützt.
  2. Tink rät Nutzern davon ab, auf Teile von Schlüsseln zuzugreifen, da dies häufig zu Kompatibilitätsproblemen führt.

In der Praxis muss natürlich manchmal gegen beide Grundsätze verstoßen werden. Hierfür bietet Tink verschiedene Mechanismen.

Zugriffstokens für geheimen Schlüssel

Um auf geheimes Schlüsselmaterial zugreifen zu können, benötigen Nutzer ein Token. Dieses ist normalerweise ein Objekt einer Klasse ohne Funktionalität. Das Token wird normalerweise durch eine Methode wie InsecureSecretKeyAccess.get() bereitgestellt. In Google wird Nutzer mithilfe der Bazel-Build-Sichtbarkeit daran gehindert, diese Funktion zu verwenden. Außerhalb von Google können Sicherheitsprüfer ihre Codebasis nach Verwendungen dieser Funktion durchsuchen.

Eine nützliche Funktion dieser Tokens ist, dass sie weitergegeben werden können. Angenommen, Sie haben eine Funktion, die einen beliebigen Tink-Schlüssel serialisiert:

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

Bei Schlüsseln mit geheimem Schlüsselmaterial darf für diese Funktion das Objekt secretKeyAccess nicht null sein und es muss ein tatsächliches SecretKeyAccess-Token gespeichert sein. Bei Schlüsseln, die kein geheimes Material haben, wird secretKeyAccess ignoriert.

Mit einer solchen Funktion ist es möglich, eine Funktion zu schreiben, die ein ganzes Keyset serialisiert: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Diese Funktion ruft intern serializeKey für jeden Schlüssel im Keyset auf und übergibt den angegebenen secretKeyAccess an die zugrunde liegende Funktion. Nutzer, die dann serializeKeyset aufrufen, ohne das Material des geheimen Schlüssels serialisieren, können null als zweites Argument verwenden. Nutzer, die geheimes Schlüsselmaterial serialisieren müssen, müssen InsecureSecretKeyAccess.get() verwenden.

Zugriff auf Teile eines Schlüssels

Ein relativ häufiger Sicherheitsfehler ist ein sogenannter Angriff auf die Wiederverwendung von Schlüsseln. Das kann vorkommen, wenn Nutzer beispielsweise den Modulus n und die Exponenten d und e eines RSA-Schlüssels in zwei verschiedenen Einstellungen wiederverwenden, z.B. zum Berechnen von Signaturen und Verschlüsselungen1.

Ein weiterer relativ häufiger Fehler bei kryptografischen Schlüsseln besteht darin, einen Teil des Schlüssels anzugeben und dann die Metadaten "anzunehmen". Angenommen, ein Nutzer möchte einen öffentlichen RSASSA-PSS-Schlüssel aus Tink zur Verwendung mit einer anderen Bibliothek exportieren. In Tink haben diese Schlüssel die folgenden Teile:

  • Modulus n
  • Der öffentliche Exponent e
  • Die Spezifikation der beiden intern verwendeten Hash-Funktionen
  • Die Länge des Salt, der intern im Algorithmus verwendet wird.

Beim Exportieren eines solchen Schlüssels werden die Hash-Funktionen und die Salt-Länge möglicherweise ignoriert. Dies funktioniert häufig gut, da oft andere Bibliotheken nicht nach den Hash-Funktionen fragen (und z. B. einfach davon ausgehen, dass SHA256 verwendet wird) und die in Tink verwendete Hash-Funktion zufällig mit der in der anderen Bibliothek identisch ist (oder vielleicht wurden die Hash-Funktionen speziell ausgewählt, damit sie mit der anderen Bibliothek zusammenarbeitet).

Trotzdem wäre es ein potenziell teurer Fehler, die Hash-Funktionen zu ignorieren. Angenommen, dem Tink-Schlüsselsatz wird später ein neuer Schlüssel mit einer anderen Hash-Funktion hinzugefügt. Angenommen, der Schlüssel wird dann mit der Methode exportiert und an einen Geschäftspartner übergeben, der ihn mit der anderen Bibliothek verwendet. Tink geht jetzt von einer anderen internen Hash-Funktion aus und kann die Signatur nicht verifizieren.

In diesem Fall sollte die Funktion, die den Schlüssel exportiert, fehlschlagen, wenn die Hash-Funktion nicht mit den Erwartungen der anderen Bibliothek übereinstimmt. Andernfalls ist der exportierte Schlüssel nutzlos, da er inkompatible Geheimtexte oder Signaturen erstellt.

Um solche Fehler zu vermeiden, schränkt Tink Funktionen ein, die Zugriff auf das Schlüsselmaterial ermöglichen. Diese sind nur teilweise, könnten aber als vollständiger Schlüssel verwechselt werden. In Java verwendet Tink zum Beispiel RestrictedApi.

Wenn ein Nutzer eine solche Annotation verwendet, ist er dafür verantwortlich, sowohl Angriffe auf die Wiederverwendung von Schlüsseln als auch Inkompatibilitäten zu verhindern.

Best Practice: Tink-Objekte beim Schlüsselimport so früh wie möglich verwenden

Am häufigsten stoßen Sie beim Exportieren von Schlüsseln oder Importieren von Schlüsseln nach Tink auf Methoden, die durch „teilweise Schlüsselzugriff“ eingeschränkt sind.

Dadurch wird das Risiko von Angriffen mit Schlüsselverwirrung minimiert, da das Tink Key-Objekt den korrekten Algorithmus vollständig angibt und alle Metadaten zusammen mit dem Schlüsselmaterial speichert.

Dazu ein Beispiel:

Nicht typisierte Nutzung:

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

Dies ist fehleranfällig: Auf der Aufrufwebsite vergisst man leicht, dass Sie dasselbe ecPoint nie mit einem anderen Algorithmus verwenden sollten. Wenn beispielsweise eine ähnliche Funktion namens encryptWithECHybridEncrypt existiert, kann der Aufrufer denselben Kurvenpunkt zum Verschlüsseln einer Nachricht verwenden, was leicht zu Sicherheitslücken führen kann.

Stattdessen ist es besser, verifyEcdsaSignature so zu ändern, dass das erste Argument EcdsaPublicKey ist. Immer, wenn der Schlüssel vom Laufwerk oder dem Netzwerk gelesen wird, sollte er sofort in ein EcdsaPublicKey-Objekt konvertiert werden. Zu diesem Zeitpunkt wissen Sie bereits, auf welche Weise der Schlüssel verwendet wird, sodass Sie ihn am besten verwenden.

Der vorherige Code lässt sich noch weiter verbessern. Anstatt ein EcdsaPublicKey zu übergeben, ist es besser, ein KeysetHandle zu übergeben. Damit wird der Code ohne zusätzlichen Aufwand für die Schlüsselrotation vorbereitet. Diese Option sollte also bevorzugt werden.

Die Verbesserungen werden jedoch nicht vorgenommen. Noch besser ist es, das PublicKeyVerify-Objekt zu übergeben. Das ist für diese Funktion ausreichend. Durch die Übergabe des PublicKeyVerify-Objekts erhöht sich also möglicherweise die Anzahl der Einsatzmöglichkeiten dieser Funktion. An dieser Stelle wird die Funktion jedoch ziemlich trivial und kann inline eingefügt werden.

Empfehlung:Wenn Schlüsselmaterial zum ersten Mal vom Laufwerk oder dem Netzwerk gelesen wird, erstellen Sie so schnell wie möglich die entsprechenden Tink-Objekte.

Verwendete Eingabe:

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

Mithilfe eines solchen Codes konvertieren wir sofort das Byte-Array in ein Tink-Objekt, wenn es gelesen wird, und wir geben vollständig an, welcher Algorithmus verwendet werden soll. Mit diesem Ansatz wird die Wahrscheinlichkeit von Angriffen durch Schlüsselverwirrung minimiert.

Best Practice: Alle Parameter des Schlüsselexports überprüfen

Wenn Sie beispielsweise eine Funktion schreiben, die einen öffentlichen HPKE-Schlüssel exportiert:

Falsche Methode zum Exportieren eines öffentlichen Schlüssels:

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

Das ist problematisch. Nach Erhalt des Schlüssels geht der Drittanbieter, der ihn verwendet, eine Annahme über die Parameter des Schlüssels an. Beispielsweise wird angenommen, dass der für diesen 256-Bit-Schlüssel verwendete HPKE AEAD-Algorithmus AES-GCM ist und so weiter.

Empfehlung:Prüfen Sie, ob die Parameter beim Schlüsselexport Ihren Erwartungen entsprechen.

Bessere Möglichkeit, einen öffentlichen Schlüssel zu exportieren:

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