Schlüsselmaterial programmatisch exportieren

Tink rät von schlechten Praktiken im Zusammenhang mit Schlüsseln ab, z. B.:

  • Nutzerzugriff auf geheimes Schlüsselmaterial: Stattdessen sollten geheime Schlüssel nach Möglichkeit in einem KMS gespeichert werden. Verwenden Sie dazu eine der vordefinierten Methoden, die Tink für solche Systeme unterstützt.
  • Nutzerzugriff auf Teile von Schlüsseln – Dies führt häufig zu Kompatibilitätsfehlern.

In der Praxis gibt es jedoch Fälle, in denen ein Verstoß gegen diese Grundsätze erforderlich ist. Tink bietet Mechanismen, um dies sicher zu machen. Sie werden in den folgenden Abschnitten beschrieben.

Zugriffstokens für geheime Schlüssel

Um auf geheimes Schlüsselmaterial zugreifen zu können, benötigen Nutzer ein Token. Das ist in der Regel ein Objekt einer bestimmten Klasse ohne Funktionen. Das Token wird normalerweise von einer Methode wie InsecureSecretKeyAccess.get() bereitgestellt. Innerhalb von Google wird die Verwendung dieser Funktion durch die Sichtbarkeit von Bazel BUILD verhindert. Außerhalb von Google können Sicherheitsprüfer in ihrer Codebasis nach Verwendungen dieser Funktion suchen.

Ein nützliches Merkmal 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 Geheimschlüsselmaterial muss für diese Funktion das secretKeyAccess-Objekt nicht null sein und ein tatsächliches SecretKeyAccess-Token gespeichert sein. Bei Schlüsseln ohne geheimes Material 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 Schlüsselsatz auf und übergibt die angegebene secretKeyAccess an die zugrunde liegende Funktion. Nutzer, die dann serializeKeyset aufrufen, ohne geheimes Schlüsselmaterial serialisieren zu müssen, können null als zweites Argument verwenden. Nutzer, die geheimes Schlüsselmaterial serialisieren müssen, sollten InsecureSecretKeyAccess.get() verwenden.

Zugriff auf Teile eines Schlüssels

Tink-Schlüssel enthalten nicht nur Rohschlüsselmaterial, sondern auch Metadaten, die angeben, wie der Schlüssel verwendet werden soll (und dass er nicht anderweitig verwendet werden darf). Ein RSA SSA PSS-Schlüssel in Tink gibt beispielsweise an, dass dieser RSA-Schlüssel nur mit dem PSS-Signaturalgorithmus mit der angegebenen Hash-Funktion und der angegebenen Salt-Länge verwendet werden darf.

Manchmal ist es erforderlich, einen Tink-Schlüssel in verschiedene Formate umzuwandeln, in denen möglicherweise nicht alle diese Metadaten explizit angegeben sind. Dies bedeutet in der Regel, dass die Metadaten bereitgestellt werden müssen, wenn der Schlüssel verwendet wird. Angenommen, der Schlüssel wird immer mit demselben Algorithmus verwendet, dann hat er implizit immer dieselben Metadaten, sie werden nur an einem anderen Ort gespeichert.

Wenn Sie einen Tink-Schlüssel in ein anderes Format konvertieren, müssen Sie darauf achten, dass die Metadaten des Tink-Schlüssels mit den (implizit angegebenen) Metadaten des anderen Schlüsselformats übereinstimmen. Andernfalls muss die Conversion fehlschlagen.

Da diese Prüfungen häufig fehlen oder unvollständig sind, schränkt Tink den Zugriff auf APIs ein, die nur teilweisen Zugriff auf das Schlüsselmaterial gewähren, aber fälschlicherweise als vollständiger Schlüssel angesehen werden könnten. In Java verwendet Tink hierfür RestrictedApi, in C++ und Golang Tokens, die den Zugriffstokens mit geheimem Schlüssel ähneln.

Die Nutzer dieser APIs sind dafür verantwortlich, sowohl Angriffe durch Schlüsselwiederverwendung als auch Inkompatibilitäten zu verhindern.

Am häufigsten werden Methoden verwendet, die den „teilweisen Schlüsselzugriff“ beim Exportieren von Schlüsseln aus oder Importieren von Schlüsseln in Tink einschränken. In den folgenden Best Practices wird beschrieben, wie Sie in diesen Fällen sicher arbeiten.

Best Practice: Alle Parameter beim Schlüsselexport prüfen

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

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

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

Das ist problematisch. Nachdem der Schlüssel empfangen wurde, nimmt der Dritte, der ihn verwendet, einige Annahmen zu den Schlüsselparametern vor: Beispielsweise wird davon ausgegangen, dass der für diesen 256-Bit-Schlüssel verwendete HPKE-AEAD-Algorithmus AES-GCM war.

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

Bessere Möglichkeit zum Exportieren eines öffentlichen Schlüssels:

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

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

Dadurch wird das Risiko von Schlüssel-Verwirrungsangriffen minimiert, da das Tink Key-Objekt vollständig den richtigen Algorithmus 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(RegistryConfiguration.get(), PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

Das ist fehleranfällig: Auf der Aufrufwebsite vergisst man leicht, dass du dieselbe ecPoint nie mit einem anderen Algorithmus verwenden solltest. Wenn beispielsweise eine ähnliche Funktion namens encryptWithECHybridEncrypt existiert, kann der Aufrufer denselben Kurvenpunkt verwenden, um eine Nachricht zu verschlüsseln, was leicht zu Sicherheitslücken führen kann.

Stattdessen ist es besser, verifyEcdsaSignature so zu ändern, dass das erste Argument EcdsaPublicKey ist. Tatsächlich sollte der Schlüssel immer dann, wenn er vom Laufwerk oder aus dem Netzwerk gelesen wird, sofort in ein EcdsaPublicKey-Objekt umgewandelt werden. An diesem Punkt wissen Sie bereits, wie der Schlüssel verwendet wird, und sollten ihn daher festlegen.

Der Code oben kann noch weiter optimiert werden. Anstatt EcdsaPublicKey zu übergeben, ist es besser, KeysetHandle zu übergeben. Der Code wird ohne zusätzliche Arbeit für die Schlüsselrotation vorbereitet. Daher sollte diese Option bevorzugt werden.

Es gibt jedoch noch weitere Verbesserungsmöglichkeiten: Es ist noch besser, das PublicKeyVerify-Objekt zu übergeben. Das ist für diese Funktion ausreichend. Wenn Sie das PublicKeyVerify-Objekt übergeben, können Sie diese Funktion möglicherweise an mehr Stellen verwenden. An diesem Punkt wird die Funktion jedoch ziemlich trivial und kann inline eingefügt werden.

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

Geschriebene Verwendung:

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

Mit diesem Code wird das Byte-Array beim Lesen sofort in ein Tink-Objekt umgewandelt und der zu verwendende Algorithmus vollständig angegeben. Dieser Ansatz minimiert die Wahrscheinlichkeit von Key-Verwirrungsangriffen.