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 オブジェクトにすぐに変換し、使用するアルゴリズムを完全に指定できます。このアプローチにより、鍵の混同攻撃の確率を最小限に抑えることができます。