アクセス制御

Tink の目標の一つは、不正な行為を阻止することです。このセクションには、特に次の 2 つのポイントがあります。

  1. Tink は、ユーザーが秘密鍵マテリアルにアクセスできないような使用を推奨しています。代わりに、Tink が該当のシステムをサポートしている事前定義された方法のいずれかを使用して、可能な限り KMS に秘密鍵を保存してください。
  2. Tink は、互換性のバグが発生することが多いため、ユーザーがキーの一部にアクセスすることを防ぎます。

実際には、この両方の原則に反することもあります。そのために、Tink にはさまざまなメカニズムが用意されています。

秘密鍵アクセス トークン

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

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

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

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

このような関数を使用すると、鍵セット全体をシリアル化する関数を作成できます。 String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

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

鍵の一部にアクセスする

比較的一般的なセキュリティ バグは「鍵再利用攻撃」です。これは、ユーザーが 2 つの異なる設定(署名と暗号化の計算など)で、たとえば n のモジュラスと、RSA 鍵の指数 de を再利用した場合に発生することがあります1

暗号鍵を扱う際に比較的よくあるミスとして、鍵の一部を指定してからメタデータを「想定」するというものがあります。たとえば、ユーザーが別のライブラリで使用するために Tink から RSASSA-PSS 公開鍵をエクスポートするとします。Tink では、これらのキーに次の部分があります。

  • モジュラス n
  • 公開指数 e
  • 内部で使用される 2 つのハッシュ関数の仕様
  • アルゴリズムの内部で使用されるソルトの長さ。

このようなキーをエクスポートする場合、ハッシュ関数とソルトの長さは無視される場合があります。他のライブラリはハッシュ関数を要求しないことが多いため(たとえば、SHA256 が使用されていると仮定)、Tink で使用されるハッシュ関数は偶然にも他のライブラリと同じ(または、他のライブラリと連携して動作するようにハッシュ関数が特別に選択された)ため、これはしばしば問題なく機能します。

それでも、ハッシュ関数を無視することは、コストがかかる可能性があるミスです。これを確認するために、別のハッシュ関数を持つ新しい鍵が後で Tink 鍵セットに追加されたとします。次に、キーがメソッドでエクスポートされ、ビジネス パートナーに渡されるとします。ビジネス パートナーは、そのキーを他のライブラリで使用します。Tink は異なる内部ハッシュ関数を前提とし、署名を検証できなくなりました。

この場合、ハッシュ関数が他のライブラリの想定と一致しない場合、鍵をエクスポートする関数は失敗します。それ以外の場合は、互換性のない暗号テキストまたは署名が作成されるため、エクスポートされた鍵は役に立ちません。

このようなミスを防ぐため、Tink は部分的なキーマテリアルにアクセスできる関数を制限しますが、このキーは部分的なものですが、完全なキーと間違えられる可能性があります。たとえば、Java Tink では、このために RestrictedApi を使用します。

ユーザーがこのようなアノテーションを使用する場合、鍵の再利用攻撃と互換性の問題の両方を防ぐ責任があります。

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

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(PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

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

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

上記のコードはさらに改善できます。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 オブジェクトに変換し、使用するアルゴリズムを完全に指定します。このアプローチにより、主要な混同攻撃の確率が最小限に抑えられます。

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

たとえば、HPKE 公開鍵をエクスポートする関数を作成する場合は、次のようにします。

公開鍵のエクスポートの悪い方法:

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

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

おすすめの方法: パラメータが鍵のエクスポートで想定どおりであることを確認します。

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

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