Exporter le matériel de clé par programmation

Tink déconseille les mauvaises pratiques liées aux clés, par exemple :

  • Accès des utilisateurs au matériel de clé secrète : à la place, les clés secrètes doivent être stockées dans un KMS dans la mesure du possible à l'aide de l'une des méthodes prédéfinies par lesquelles Tink prend en charge ces systèmes.
  • Accès des utilisateurs à des parties de clés : cela entraîne souvent des bugs de compatibilité.

Dans la pratique, il arrive que nous devions enfreindre ces principes. Tink fournit des mécanismes pour pouvoir le faire de manière sécurisée, qui sont décrits dans les sections suivantes.

Jetons d'accès avec clé secrète

Pour accéder au matériel de clé secrète, les utilisateurs doivent disposer d'un jeton (qui est généralement un objet d'une classe, sans aucune fonctionnalité). Le jeton est généralement fourni par une méthode telle que InsecureSecretKeyAccess.get(). Dans Google, les utilisateurs ne peuvent pas utiliser cette fonction à l'aide de la visibilité BUILD Bazel. En dehors de Google, les examinateurs de sécurité peuvent rechercher dans leur codebase les utilisations de cette fonction.

L'une des fonctionnalités utiles de ces jetons est qu'ils peuvent être transmis. Par exemple, supposons que vous disposiez d'une fonction qui sérialise une clé Tink arbitraire :

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

Pour les clés qui disposent d'un matériel de clé secrète, cette fonction nécessite que l'objet secretKeyAccess ne soit pas nul et qu'un jeton SecretKeyAccess réel soit stocké. Pour les clés qui ne disposent d'aucun matériel secret, secretKeyAccess est ignoré.

Compte tenu d'une telle fonction, il est possible d'écrire une fonction qui sérialise l'ensemble d'une collection de clés :

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

Cette fonction appelle serializeKey pour chaque clé du jeu de clés en interne et transmet le secretKeyAccess donné à la fonction sous-jacente. Les utilisateurs qui appellent ensuite serializeKeyset sans avoir à sérialiser le matériel de clé secrète peuvent utiliser null comme deuxième argument. Les utilisateurs qui doivent sérialiser du matériel de clé secrète doivent utiliser InsecureSecretKeyAccess.get().

Accès aux parties d'une clé

Les clés Tink ne contiennent pas seulement du matériel de clé brut, mais également des métadonnées qui spécifient comment la clé doit être utilisée (et, à son tour, qu'elle ne doit pas être utilisée d'une autre manière). Par exemple, une clé RSA SSA PSS dans Tink spécifie que cette clé RSA ne peut être utilisée qu'avec l'algorithme de signature PSS à l'aide de la fonction de hachage et de la longueur de sel spécifiées.

Il est parfois nécessaire de convertir une clé Tink dans différents formats qui ne spécifient pas explicitement toutes ces métadonnées. Cela signifie généralement que les métadonnées doivent être fournies lors de l'utilisation de la clé. En d'autres termes, en supposant que la clé est toujours utilisée avec le même algorithme, une telle clé possède toujours implicitement les mêmes métadonnées, mais elles sont simplement stockées à un autre endroit.

Lorsque vous convertissez une clé Tink dans un autre format, vous devez vous assurer que les métadonnées de la clé Tink correspondent aux métadonnées (spécifiées implicitement) de l'autre format de clé. Si ce n'est pas le cas, la conversion doit échouer.

Étant donné que ces vérifications sont souvent manquantes ou incomplètes, Tink limite l'accès aux API qui donnent accès au matériel de clé qui n'est que partiel, mais qui pourrait être confondu avec une clé complète. En Java, Tink utilise RestrictedApi pour cela. En C++ et Golang, il utilise des jetons semblables aux jetons d'accès de la clé secrète.

Les utilisateurs de ces API sont tenus d'empêcher à la fois les attaques par réutilisation de clé et les incompatibilités.

Vous rencontrez le plus souvent des méthodes qui limitent l'accès partiel aux clés dans le contexte de l'exportation ou de l'importation de clés vers Tink. Les bonnes pratiques suivantes expliquent comment utiliser ces scénarios de manière sécurisée.

Bonne pratique: vérifier tous les paramètres lors de l'exportation de la clé

Par exemple, si vous écrivez une fonction qui exporte une clé publique HPKE :

Mauvaise façon d'exporter une clé publique :

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

Cela pose problème. Après avoir reçu la clé, le tiers qui l'utilise émet des hypothèses sur ses paramètres : par exemple, il suppose que l'algorithme AEAD HPKE utilisé pour cette clé de 256 bits était AES-GCM.

Recommandation : Vérifiez que les paramètres correspondent à vos attentes lors de l'exportation des clés.

Meilleure façon d'exporter une clé publique:

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

Bonne pratique : utilisez les objets Tink dès que possible lors de l'importation des clés

Cela réduit le risque d'attaques par confusion de clé, car l'objet de clé Tink spécifie entièrement l'algorithme approprié et stocke toutes les métadonnées avec le matériel de clé.

Prenons l'exemple suivant :

Utilisation non typée :

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

Cette situation est sujette aux erreurs: sur le site d'appel, il est très facile d'oublier que vous ne devez jamais utiliser le même ecPoint avec un autre algorithme. Par exemple, si une fonction similaire appelée encryptWithECHybridEncrypt existe, l'appelant peut utiliser le même point de courbe pour chiffrer un message, ce qui peut facilement entraîner des failles.

Il est préférable de modifier verifyEcdsaSignature pour que le premier argument soit EcdsaPublicKey. En fait, chaque fois que la clé est lue à partir du disque ou du réseau, elle doit être immédiatement convertie en objet EcdsaPublicKey. À ce stade, vous savez déjà de quelle manière la clé est utilisée. Il est donc préférable de vous y engager.

Le code précédent peut être encore amélioré. Au lieu de transmettre un EcdsaPublicKey, il est préférable de transmettre un KeysetHandle. Il prépare le code à la rotation des clés sans effort supplémentaire. C'est donc la solution à privilégier.

Les améliorations ne sont toutefois pas terminées : il est encore préférable de transmettre l'objet PublicKeyVerify : cela suffit pour cette fonction. Transmettre l'objet PublicKeyVerify augmente donc potentiellement les endroits où cette fonction peut être utilisée. À ce stade, cependant, la fonction devient plutôt triviale et peut être intégrée.

Recommandation : Lorsque le matériel de clé est lu à partir du disque ou du réseau pour la première fois, créez les objets Tink correspondants dès que possible.

Utilisation saisie :

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

À l'aide de ce code, nous convertissons immédiatement le tableau d'octets en objet Tink lorsqu'il est lu, et nous spécifions complètement l'algorithme à utiliser. Cette approche réduit le risque d'attaques par confusion de clés.