鍵マテリアルをプログラムでエクスポートする

Tink では、次のようなキーに関連する不適切な方法は推奨されていません。

  • 秘密鍵マテリアルへのユーザー アクセス - 代わりに、秘密鍵は、Tink がそのようなシステムをサポートする事前定義された方法のいずれかを使用して、可能な限り KMS に保存する必要があります。
  • キーの一部へのユーザー アクセス - 多くの場合、互換性に関するバグが発生します。

実際には、これらの原則に違反する必要がある場合があります。Tink には、これを安全に行うためのメカニズムが用意されています。これについては、次のセクションで説明します。

シークレット キー アクセス トークン

秘密鍵マテリアルにアクセスするには、ユーザーがトークン(通常は機能のないクラスのオブジェクト)を持っている必要があります。通常、トークンは InsecureSecretKeyAccess.get() などのメソッドによって提供されます。Google では、Bazel BUILD の公開設定を使用して、ユーザーがこの関数を使用できないようにしています。Google の外部では、セキュリティ審査担当者はコードベースでこの機能の使用状況を検索できます。

これらのトークンの便利な機能の一つは、トークンを渡せることです。たとえば、任意の Tink キーをシリアル化する関数があるとします。

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

秘密鍵マテリアルを持つ鍵の場合、この関数では secretKeyAccess オブジェクトが null ではなく、実際の SecretKeyAccess トークンが保存されている必要があります。シークレット マテリアルのない鍵の場合、secretKeyAccess は無視されます。

このような関数を使用すると、鍵セット全体をシリアル化する関数を作成できます。

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

この関数は、内部で鍵セット内の各キーに対して serializeKey を呼び出し、指定された secretKeyAccess を基となる関数に渡します。秘密鍵マテリアルをシリアル化せずに serializeKeyset を呼び出すユーザーは、2 番目の引数として null を使用できます。秘密鍵マテリアルをシリアル化する必要があるユーザーは、InsecureSecretKeyAccess.get() を使用する必要があります。

鍵の一部へのアクセス

Tink 鍵には、未加工の鍵マテリアルだけでなく、鍵の使用方法(および他の方法で使用すべきでない方法)を指定するメタデータも含まれています。たとえば、Tink の RSA SSA PSS 鍵では、この RSA 鍵は、指定されたハッシュ関数と指定されたソルト長を使用して PSS 署名アルゴリズムでのみ使用できることが指定されています。

場合によっては、Tink キーをさまざまな形式に変換する必要があるため、このメタデータのすべてを明示的に指定しない場合があります。通常、これは鍵を使用するときにメタデータを指定する必要があることを意味します。つまり、鍵が常に同じアルゴリズムで使用されていると仮定すると、そのような鍵には暗黙的に同じメタデータが含まれ、保存場所が異なるだけです。

Tink キーを別の形式に変換する場合は、Tink キーのメタデータが、他のキー形式の(暗黙的に指定された)メタデータと一致していることを確認する必要があります。一致しない場合、変換は失敗する必要があります。

多くの場合、これらのチェックが欠落しているか不完全であるため、Tink は、部分的な鍵マテリアルにアクセスできる API へのアクセスを制限します。この鍵マテリアルは完全な鍵と誤認される可能性があります。Java では、Tink はこれに RestrictedApi を使用します。C++ と Golang では、秘密鍵アクセス トークンに似たトークンを使用します。

これらの API のユーザーは、鍵の再利用攻撃と互換性の両方を防ぐ責任があります。

最も一般的な方法は、Tink から鍵をエクスポートまたは Tink に鍵をインポートするコンテキストで「部分的な鍵アクセス」を制限する方法です。次のベスト プラクティスでは、このようなシナリオで安全に運用する方法について説明します。

ベスト プラクティス: 鍵のエクスポートですべてのパラメータを確認する

たとえば、HPKE 公開鍵をエクスポートする関数を作成する場合:

公開鍵をエクスポートする不適切な方法:

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

これは問題です。鍵を受け取ったサードパーティは、鍵のパラメータについていくつかの仮定を行います。たとえば、この 256 ビット鍵に使用された HPKE AEAD アルゴリズムが AES-GCM であると仮定します。

推奨: パラメータが鍵のエクスポートで期待どおりであることを確認します。

公開鍵をエクスポートするより良い方法:

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

ベスト プラクティス: 鍵のインポートではできるだけ早く Tink オブジェクトを使用する

これにより、Tink Key オブジェクトが正しいアルゴリズムを完全に指定し、すべてのメタデータを鍵マテリアルとともに保存するため、鍵混同攻撃のリスクを最小限に抑えることができます。

たとえば次のようになります。

型なしの使用:

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

これはエラーが発生しやすいです。呼び出し元で、同じ ecPoint を別のアルゴリズムと使用しないことを忘れがちです。たとえば、encryptWithECHybridEncrypt という類似の関数が存在する場合、呼び出し元は同じ曲線ポイントを使用してメッセージを暗号化する可能性があります。これは、脆弱性につながる可能性があります。

代わりに、最初の引数が EcdsaPublicKey になるように verifyEcdsaSignature を変更することをおすすめします。実際、鍵がディスクまたはネットワークから読み取られる場合は、直ちに EcdsaPublicKey オブジェクトに変換される必要があります。この時点で、鍵の用途はすでにわかっているため、コミットすることをおすすめします。

上記のコードはさらに改善できます。EcdsaPublicKey ではなく KeysetHandle を渡すほうが適切です。追加作業なしで鍵のローテーション用のコードを準備します。そのため、この方法をおすすめします。

ただし、改善は行われていません。PublicKeyVerify オブジェクトを渡すことをおすすめします。この関数にはこれで十分です。PublicKeyVerify オブジェクトを渡すと、この関数を使用できる場所が増える可能性があります。ただし、この時点で関数はかなり単純になり、インライン化できます。

推奨事項: 鍵マテリアルがディスクまたはネットワークから初めて読み取られた場合は、対応する Tink オブジェクトをできるだけ早く作成します。

型付きの使用例:

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

このようなコードを使用すると、読み取り時にバイト配列を Tink オブジェクトにすぐに変換し、使用するアルゴリズムを完全に指定できます。このアプローチにより、鍵の混同攻撃の確率を最小限に抑えることができます。