Esporta in modo programmatico materiale chiave

Tink scoraggia le pratiche scorrette 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 classe senza alcuna funzionalità). In genere il token viene fornito con un metodo come InsecureSecretKeyAccess.get(). All'interno di Google, agli utenti non è consentito utilizzare questa funzione utilizzando la visibilità Bazel BUILD. 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, supponiamo che tu abbia una funzione che serializza una chiave Tink arbitraria:

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

Per le chiavi che contengono materiale della chiave segreta, questa funzione richiede che l'oggetto secretKeyAccess sia non nullo e che abbia un token SecretKeyAccess effettivo archiviato. Per le chiavi che non hanno materiale segreto, 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 nel set di chiavi e passa il valore 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 usare InsecureSecretKeyAccess.get().

Accesso di parti di una chiave

Le chiavi Tink non contengono solo il materiale della chiave non elaborato, ma anche metadati che specificano come deve essere utilizzata la chiave (e, di conseguenza, che non deve essere utilizzata in nessun altro modo). Ad esempio, una chiave RSA SSA PSS in Tink specifica che questa chiave RSA può essere utilizzata solo con l'algoritmo di firma PSS che utilizza la funzione hash specificata e la lunghezza del sale specificata.

A volte è necessario convertire una chiave Tink in diversi formati che potrebbero non specificare in modo esplicito tutti questi metadati. Questo di solito 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 ai metadati (specificati implicitamente) 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 ai token di accesso con chiave segreta.

Gli utenti di queste API sono responsabili della prevenzione sia degli attacchi di riutilizzo delle chiavi sia delle incompatibilità.

Solitamente riscontri metodi che limitano l'"accesso parziale alla chiave" nel contesto dell'esportazione di chiavi o dell'importazione di chiavi su 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:

Modo sbagliato 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 supposizioni sui relativi parametri: ad esempio, presume che l'algoritmo HPKE AEAD utilizzato per questa chiave a 256 bit sia AES-GCM.

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

Metodo 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 relativi alle 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 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 chiamata encryptWithECHybridEncrypt, l'utente che chiama potrebbe utilizzare lo stesso punto della curva per criptare un messaggio, il che può facilmente portare a 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 migliorato ulteriormente. Invece di passare in un EcdsaPublicKey, è meglio passare in un KeysetHandle. Prepara il codice per la rotazione delle chiavi senza alcun intervento aggiuntivo. Dovrebbe quindi essere preferibile questa soluzione.

Tuttavia, i miglioramenti non sono stati apportati: è ancora meglio passare l'oggetto PublicKeyVerify: questo è sufficiente per questa funzione, quindi il passaggio dell'oggetto PublicKeyVerify aumenta potenzialmente le posizioni 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 per la prima volta dal disco o dalla rete, 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.