Controllo dell'accesso

Un obiettivo di Tink è scoraggiare le pratiche scorrette. Di particolare interesse in questa sezione sono due punti:

  1. Tink incoraggia l'utilizzo in modo che gli utenti non possano accedere al materiale della chiave secret. Le chiavi private devono invece essere archiviate in un KMS, ove possibile, utilizzando uno dei modi predefiniti in cui Tink supporta questi sistemi.
  2. Tink dissuade gli utenti dall'accesso a parti delle chiavi, in quanto ciò comporta spesso bug di compatibilità.

In pratica, ovviamente a volte entrambi questi principi devono essere violati. Per questo, Tink offre diversi meccanismi.

Token di accesso alle chiavi dei secret

Per accedere al materiale delle chiavi private, gli utenti devono disporre di un token (che in genere è semplicemente un oggetto di una classe, senza funzionalità). Il token viene in genere fornito da un metodo come InsecureSecretKeyAccess.get(). All'interno di Google, gli utenti non possono utilizzare questa funzione tramite la visibilità di Bazel Build. Al di fuori di Google, i revisori della sicurezza possono cercare gli utilizzi di questa funzione nel loro codebase.

Una caratteristica utile di questi token è che possono essere trasmessi. Ad esempio, supponi di avere una funzione che serializza una chiave Tink arbitraria:

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

Per le chiavi che contengono materiale della chiave secret, questa funzione richiede che l'oggetto secretKeyAccess non sia null e disponga di un token SecretKeyAccess effettivo. Per le chiavi che non contengono materiale secret, il valore secretKeyAccess viene ignorato.

Data una funzione di questo tipo, è possibile scrivere una funzione che serializza un intero set di chiavi: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Questa funzione chiama internamente serializeKey per ogni chiave del set di chiavi e trasmette il valore secretKeyAccess specificato alla funzione sottostante. Gli utenti che chiamano serializeKeyset senza dover serializzare il materiale della chiave privata possono usare null come secondo argomento. Gli utenti che hanno la necessità di serializzare il materiale della chiave secret devono usare InsecureSecretKeyAccess.get().

Accesso a parti di una chiave

Un bug di sicurezza relativamente comune è un "attacco di riutilizzo delle chiavi". Questo può verificarsi quando gli utenti riutilizzano, ad esempio, il modulo n e gli esponenti d e e di una chiave RSA in due impostazioni diverse (ad es. per calcolare firme e crittografia)1.

Un altro errore relativamente comune nella gestione delle chiavi di crittografia è specificare una parte della chiave, quindi "supporre" i metadati. Ad esempio, supponiamo che un utente voglia esportare una chiave pubblica RSASSA-PSS da Tink per utilizzarla con una libreria diversa. In Tink, queste chiavi sono composte dalle seguenti parti:

  • Il modulo n
  • L'esponente pubblico e
  • La specifica delle due funzioni hash utilizzate internamente
  • La lunghezza del sale utilizzato internamente nell'algoritmo.

Durante l'esportazione di una chiave di questo tipo, potresti ignorare le funzioni hash e la lunghezza del sale. Questo spesso può andare bene, poiché spesso altre librerie non chiedono le funzioni hash (e ad esempio presuppongono semplicemente che venga utilizzato l'algoritmo SHA256) e la funzione hash utilizzata in Tink è per coincidenza la stessa dell'altra libreria (o forse le funzioni hash sono state scelte specificamente in modo che funzioni insieme all'altra libreria).

Tuttavia, ignorare le funzioni hash sarebbe un errore potenzialmente costoso. Per vedere questo risultato, supponiamo che in un secondo momento venga aggiunta una nuova chiave con una funzione hash diversa al set di chiavi Tink. Supponiamo che, quindi, la chiave venga esportata con il metodo e data a un partner commerciale, che la utilizza con l'altra libreria. Tink ora presuppone una funzione hash interna diversa e non può verificare la firma.

In questo caso, la funzione che esporta la chiave non dovrebbe riuscire se la funzione hash non corrisponde a quanto previsto dall'altra libreria; in caso contrario, la chiave esportata è inutile perché crea firme o testi criptati incompatibili.

Per evitare questo tipo di errori, Tink limita le funzioni che danno accesso al materiale della chiave, che è solo parziale, ma che potrebbe essere scambiato per una chiave completa. Ad esempio, in Java Tink utilizza RestrictedApi per questo motivo.

Quando un utente utilizza un'annotazione di questo tipo, ha la responsabilità di evitare sia gli attacchi di riutilizzo delle chiavi sia le incompatibilità.

Best practice: utilizza gli oggetti Tink il prima possibile durante l'importazione della chiave

In genere, riscontri metodi che sono limitati dall'"accesso parziale alle chiavi" durante l'esportazione di chiavi da o l'importazione di chiavi in Tink.

Questo riduce al minimo il rischio di attacchi di confusione delle chiavi, poiché l'oggetto Tink Key specifica completamente l'algoritmo corretto e archivia tutti i metadati insieme al materiale della chiave.

Considera l'esempio seguente:

Utilizzo senza tipo di digitazione:

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

Questo è soggetto a errori: sul sito di chiamata è molto facile dimenticarsi di non utilizzare mai lo stesso ecPoint con un altro algoritmo. Ad esempio, se esiste una funzione simile chiamata encryptWithECHybridEncrypt, il chiamante potrebbe utilizzare lo stesso punto della curva per criptare un messaggio, il che può causare facilmente vulnerabilità.

È preferibile modificare verifyEcdsaSignature in modo che il primo argomento sia EcdsaPublicKey. Infatti, ogni volta che la chiave viene letta dal disco o dalla rete, deve essere immediatamente convertita in un oggetto EcdsaPublicKey: a questo punto sai già in che modo viene utilizzata la chiave, quindi ti consigliamo di eseguire il commit.

Il codice precedente può essere ulteriormente migliorato. Invece di trasmettere un EcdsaPublicKey, è meglio passare una KeysetHandle. Il programma prepara il codice per la rotazione della chiave senza alcuna azione aggiuntiva. Questa dovrebbe essere la soluzione preferita.

Tuttavia, i miglioramenti non vengono apportati. È ancora meglio trasferire l'oggetto PublicKeyVerify: è sufficiente per questa funzione, quindi passare l'oggetto PublicKeyVerify aumenta potenzialmente le posizioni in cui è possibile utilizzare questa funzione. A questo punto, tuttavia, la funzione diventa piuttosto banale e può essere incorporata.

Suggerimento: quando il materiale della chiave viene letto dal disco o dalla rete per la prima volta, crea gli oggetti Tink corrispondenti il prima possibile.

Utilizzo digitato:

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

Utilizzando questo codice, convertiamo immediatamente l'array byte in un oggetto Tink quando viene letto e specifichiamo completamente l'algoritmo da usare. Questo approccio riduce al minimo la probabilità di attacchi chiave di confusione.

Best practice: verifica tutti i parametri nell'esportazione della chiave

Ad esempio, se scrivi una funzione che esporta una chiave pubblica HPKE:

Metodo non valido per esportare una chiave pubblica:

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

Questo può creare problemi. Dopo aver ricevuto la chiave, la terza parte che la utilizza fa alcune ipotesi sui parametri della chiave: ad esempio, presumerà che l'algoritmo HPKE AEAD utilizzato per questa chiave a 256 bit fosse AES-GCM e così via.

Consiglio:verifica che i parametri siano quelli previsti nell'esportazione della chiave.

Un modo migliore per esportare una chiave pubblica:

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