Esporta in modo programmatico materiale chiave

Tink sconsiglia le cattive pratiche relative alle chiavi, ad esempio:

  • Accesso utente al materiale della chiave segreta: le chiavi segrete devono essere memorizzate in un KMS, se possibile, utilizzando uno dei modi predefiniti in cui Tink supporta questi sistemi.
  • Accesso degli utenti a parti delle chiavi: questa operazione spesso genera bug di compatibilità.

In realtà, ci sono casi che richiedono la violazione di questi principi. Tink fornisce meccanismi per farlo in sicurezza, descritti nelle sezioni seguenti.

Token di accesso con chiave segreta

Per accedere al materiale della chiave segreta, gli utenti devono disporre di un token (che è solitamente un oggetto di una certa classe, senza alcuna funzionalità). Il token viene solitamente fornito da un metodo come InsecureSecretKeyAccess.get(). In Google, gli utenti non possono utilizzare questa funzione utilizzando la visibilità BUILD di Bazel. Al di fuori di Google, i revisori della sicurezza possono cercare nel loro codebase gli utilizzi di questa funzione.

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

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

Per le chiavi con materiale della chiave privata, questa funzione richiede che l'oggetto secretKeyAccess sia non nullo e che sia memorizzato un token SecretKeyAccess effettivo. Per le chiavi che non hanno materiale segreto, il valore secretKeyAccess viene ignorato.

Data una funzione di questo tipo, è possibile scrivere una funzione che serializzi un intero insieme di chiavi:

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

Questa funzione chiama serializeKey per ogni chiave nel set di chiavi internamente e trasmette il secretKeyAccess specificato alla funzione sottostante. Gli utenti che chiamano serializeKeyset senza dover serializzare il materiale della chiave segreta possono usare null come secondo argomento. Gli utenti che devono serializzare il materiale della chiave segreta devono utilizzare InsecureSecretKeyAccess.get().

Accesso di parti di una chiave

Le chiavi Tink non contengono solo il materiale della chiave non elaborata, ma anche metadati che specificano come deve essere utilizzata la chiave (e, a sua volta, che non dovrebbe essere utilizzata in altri modi). Ad esempio, una chiave RSA SSA PSS in Tink specifica che questa chiave RSA può essere utilizzata solo con l'algoritmo di firma PSS utilizzando la funzione hash e la lunghezza del sale specificati.

A volte è necessario convertire una chiave Tink in formati diversi che potrebbero non specificare esplicitamente tutti questi metadati. In genere, significa che i metadati devono essere forniti quando viene utilizzata la chiave. In altre parole, supponendo che la chiave venga sempre utilizzata con lo stesso algoritmo, questa chiave ha implicitamente gli stessi metadati, ma è archiviata in un luogo diverso.

Quando converti una chiave Tink in un formato diverso, devi assicurarti che i metadati della chiave Tink corrispondano a quelli (specificati in modo implicito) dell'altro formato della chiave. In caso contrario, la conversione deve non andare a buon fine.

Poiché questi controlli sono spesso mancanti o incompleti, Tink limita l'accesso alle API che forniscono l'accesso al materiale della chiave solo parziale, ma che potrebbero essere scambiate per una chiave completa. In Java, Tink utilizza RestrictedApi per questo scopo, mentre in C++ e Golang utilizza token simili a quelli di accesso alla chiave segreta.

Gli utenti di queste API sono responsabili di prevenire sia gli attacchi di riutilizzo delle chiavi sia le incompatibilità.

Molto spesso si incontrano metodi che limitano l'accesso "parziale alle chiavi" nel contesto dell'esportazione o dell'importazione di chiavi in Tink. Le seguenti best practice spiegano come operare in sicurezza in questi scenari.

Best practice: verifica tutti i parametri per l'esportazione delle chiavi

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

Metodo errato per esportare una chiave pubblica:

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

Questo è un problema. Dopo aver ricevuto la chiave, la terza parte che la utilizza fa alcune ipotesi sui parametri della chiave: ad esempio, presume che l'algoritmo AEAD HPKE utilizzato per questa chiave a 256 bit sia AES-GCM.

Consiglio: verifica che i parametri siano quelli che ti aspetti per l'esportazione delle chiavi.

Modo migliore per esportare una chiave pubblica:

/** 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: utilizza gli oggetti Tink il prima possibile durante l'importazione delle chiavi

In questo modo si riduce al minimo il rischio di attacchi di confusione delle chiavi, perché l'oggetto Tink Key specifica completamente l'algoritmo corretto e memorizza tutti i metadati insieme al materiale della chiave.

Considera l'esempio seguente:

Utilizzo non tipizzato:

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

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

È invece 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 è meglio impegnarla.

Il codice precedente può essere ulteriormente migliorato. È preferibile passare un KeysetHandle anziché un EcdsaPublicKey. Prepara il codice per la rotazione delle chiavi senza alcun intervento aggiuntivo. Dovrebbe quindi essere preferibile questa soluzione.

Tuttavia, i miglioramenti non sono finiti: è ancora meglio passare l'oggetto PublicKeyVerify: questo è sufficiente per questa funzione, quindi il passaggio dell'oggetto PublicKeyVerify aumenta potenzialmente i luoghi in cui questa funzione può essere utilizzata. A questo punto, tuttavia, la funzione diventa piuttosto banale e può essere in linea.

Consiglio: 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 il byte array in un oggetto Tink quando viene letto, specificando completamente l'algoritmo da utilizzare. Questo approccio riduce al minimo la probabilità di attacchi di confusione delle chiavi.