Contrôle des accès

L'un des objectifs de Tink est de décourager les mauvaises pratiques. Cette section présente deux points particulièrement intéressants:

  1. Tink encourage l'utilisation de telle sorte que les utilisateurs ne puissent pas accéder au matériel de la clé secrète. À la place, les clés secrètes doivent être stockées dans un KMS dans la mesure du possible en utilisant l'une des méthodes prédéfinies par lesquelles Tink prend en charge de tels systèmes.
  2. Tink empêche les utilisateurs d'accéder à certaines parties des clés, car cela entraîne souvent des bugs de compatibilité.

Dans la pratique, il faut parfois enfreindre ces deux principes. À cet effet, Tink fournit différents mécanismes.

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

Pour accéder au matériel de clé secrète, les utilisateurs doivent disposer d'un jeton (qui n'est généralement qu'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é de Bazel BUILD. En dehors de Google, les examinateurs de sécurité peuvent rechercher des utilisations de cette fonction dans leur codebase.

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

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

Pour les clés comportant du 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 n'ont aucun matériel secret, la méthode secretKeyAccess est ignorée.

Une telle fonction permet d'écrire une fonction qui sérialise une collection de clés entière : String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Cette fonction appelle serializeKey pour chaque clé de la collection 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 la clé secrète peuvent utiliser null comme deuxième argument. Les utilisateurs ayant besoin de sérialiser le matériel de clé secrète doivent utiliser InsecureSecretKeyAccess.get().

Accès à certaines parties d'une clé

Un bogue de sécurité relativement courant est une "attaque par réutilisation de clés". Cela peut se produire lorsque les utilisateurs réutilisent, par exemple, le module n et les exposants d et e d'une clé RSA dans deux paramètres différents (par exemple, pour calculer les signatures et les chiffrements)1.

Une autre erreur relativement courante concernant les clés cryptographiques consiste à spécifier une partie de la clé, puis à "assumer" les métadonnées. Par exemple, supposons qu'un utilisateur souhaite exporter une clé publique RSASSA-PSS depuis Tink pour l'utiliser avec une autre bibliothèque. Dans Tink, ces clés se composent des parties suivantes:

  • Module n
  • L'exposant public e
  • La spécification des deux fonctions de hachage utilisées en interne
  • Longueur du salage utilisé en interne dans l'algorithme.

Lorsque vous exportez une clé de ce type, vous pouvez ignorer les fonctions de hachage et la longueur de salage. Cela peut souvent fonctionner correctement, car il arrive souvent que d'autres bibliothèques ne demandent pas les fonctions de hachage (par exemple, en supposant que SHA256 est utilisé). Par ailleurs, la fonction de hachage utilisée dans Tink est la même que dans l'autre bibliothèque (ou que les fonctions de hachage aient été choisies spécifiquement pour fonctionner avec l'autre bibliothèque).

Néanmoins, ignorer les fonctions de hachage s'avérerait potentiellement coûteux. Pour le voir, supposons qu'une nouvelle clé avec une fonction de hachage différente soit ajoutée ultérieurement à la collection de clés Tink. Supposons ensuite que la clé soit exportée avec la méthode et donnée à un partenaire commercial, qui l'utilise avec l'autre bibliothèque. Tink suppose désormais une autre fonction de hachage interne et ne peut pas valider la signature.

Dans ce cas, la fonction d'exportation de la clé doit échouer si la fonction de hachage ne correspond pas aux attentes de l'autre bibliothèque. Sinon, la clé exportée est inutile, car elle crée des textes chiffrés ou des signatures incompatibles.

Pour éviter de telles erreurs, Tink limite les fonctions qui donnent accès au matériel de clé, qui n'est que partiel, mais qui peut être considéré comme une clé complète. Par exemple, en Java, Tink utilise RestrictedApi à cette fin.

Lorsqu'un utilisateur se sert d'une telle annotation, il est responsable de prévenir les attaques par réutilisation de clés et les incompatibilités.

Bonne pratique: utiliser les objets Tink le plus tôt possible lors de l'importation de la clé

Le plus souvent, vous rencontrez des méthodes limitées par un "accès partiel aux clés" lorsque vous exportez des clés depuis ou importez des clés vers Tink.

Cela réduit le risque d'attaques de confusion par clé, car l'objet Tink Key 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(PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

Cette méthode est sujette aux erreurs: au niveau du 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 afin 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 la valider.

Le code précédent peut encore être amélioré. Au lieu de transmettre un EcdsaPublicKey, il est préférable de transmettre un KeysetHandle. Elle prépare le code pour la rotation des clés sans aucune action supplémentaire. Il est donc préférable de procéder ainsi.

Toutefois, les améliorations ne sont pas terminées : il est encore préférable de transmettre l'objet PublicKeyVerify. C'est suffisant pour cette fonction. Par conséquent, la transmission de l'objet PublicKeyVerify augmente potentiellement les possibilités d'utilisation de cette fonction. À ce stade, cependant, la fonction devient relativement simple et peut être intégrée.

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

Utilisation typée:

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 lors de sa lecture et nous spécifions l'algorithme à utiliser. Cette approche réduit le risque d'attaques de confusion par clé.

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

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

Mauvaise méthode pour exporter une clé publique:

/** Provide the key to our users which do not 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 HPKE AEAD utilisé pour cette clé 256 bits est AES-GCM, et ainsi de suite.

Recommandation:Vérifiez que les paramètres correspondent à ceux attendus pour l'exportation de la clé.

Meilleure méthode pour exporter une clé publique:

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