تصدير المواد الأساسية آليًا

لا ينصح فريق Tink بالممارسات السيئة المتعلقة بالمفاتيح، مثل:

  • وصول المستخدم إلى مادة المفتاح السري: بدلاً من ذلك، يجب تخزين المفاتيح السرية في إدارة مفاتيح التشفير كلما أمكن ذلك باستخدام إحدى الطرق المحدّدة مسبقًا التي توفّرها Tink لمثل هذه الأنظمة.
  • وصول المستخدم إلى أجزاء من المفاتيح: يؤدي ذلك غالبًا إلى ظهور أخطاء في التوافق.

في الواقع، هناك حالات تتطلّب مخالفة هذه المبادئ. توفّر أداة Tink آليات لإجراء ذلك بأمان، كما هو موضّح في القسم التالي.

رموز الوصول إلى المفتاح السري

للوصول إلى مادة المفتاح السري، يجب أن يكون لدى المستخدمين رمز مميّز (وهو عادةً عنصر من فئة معيّنة بدون أي وظيفة). يتم عادةً تقديم الرمز المميّز باستخدام طريقة مثل InsecureSecretKeyAccess.get(). في Google، يتم منع المستخدمين من استخدام هذه الدالة باستخدام مستوى عرض Bazel BUILD. خارج Google، يمكن لمراجعي الأمان البحث في قاعدة الرموز البرمجية الخاصة بهم عن استخدامات هذه الدالة.

من الميزات المفيدة لهذه الرموز أنّه يمكن تمريرها. على سبيل المثال، لنفترض أنّ لديك دالة تُسلسل مفتاح Tink عشوائيًا:

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

بالنسبة إلى المفاتيح التي تحتوي على مادة مفتاح سري، تتطلّب هذه الدالة أن يكون كائن secretKeyAccess غير خالٍ وأن يحتوي على رمز SecretKeyAccess مخزّن. بالنسبة إلى المفاتيح التي لا تحتوي على أي مواد سرية، يتم تجاهل القيمة secretKeyAccess.

استنادًا إلى هذه الدالة، من الممكن كتابة دالة تسلسل ملف برمجيًا لسلسلة مفاتيح كاملة:

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

تستدعي هذه الدالة serializeKey لكل مفتاح في مجموعة المفاتيح داخليًا، و تُمرِّر secretKeyAccess المحدَّد إلى الدالة الأساسية. يمكن للمستخدمين الذين يلي ذلك استدعاء serializeKeyset بدون الحاجة إلى تسلسل مادة المفتاح السري استخدام null كوسيطة ثانية. على المستخدمين الذين يحتاجون إلى تسلسل مادة المفتاح السري استخدام InsecureSecretKeyAccess.get().

الوصول إلى أجزاء من مفتاح

لا تحتوي مفاتيح Tink على مادة المفتاح الأولية فحسب، بل تحتوي أيضًا على بيانات وصفية تحدّد كيفية استخدام المفتاح (وبالتالي، يجب عدم استخدامه بأي شكلٍ آخر). على سبيل المثال، يحدِّد مفتاح RSA SSA PSS في Tink أنّه لا يمكن استخدام مفتاح RSA هذا إلا مع خوارزمية توقيع PSS باستخدام وظيفة التجزئة المحدَّدة وطُول الملح المحدَّد.

في بعض الأحيان، يكون من الضروري تحويل مفتاح Tink إلى تنسيقات مختلفة قد لا تحدّد صراحةً كل هذه البيانات الوصفية. يعني ذلك عادةً أنّه يجب تقديم البيانات الوصفية عند استخدام المفتاح. بعبارة أخرى، بافتراض أنّه يتم استخدام المفتاح دائمًا مع الخوارزمية نفسها، سيظل هذا المفتاح يتضمّن بشكل ضمني البيانات الوصفية نفسها، ولكن يتم تخزينها في مكان مختلف.

عند تحويل مفتاح Tink إلى تنسيق مختلف، عليك التأكّد من أنّ البيانات الوصفية لمفتاح Tink تتطابق مع البيانات الوصفية (المحدّدة بشكل ضمني) لتنسيق المفتاح الآخر. وفي حال عدم تطابقهما، يجب أن تُعرَض الإحالة الناجحة على أنّها غير ناجحة.

ولأنّ عمليات التحقّق هذه غالبًا ما تكون غير متوفّرة أو غير مكتملة، تفرض Tink قيودًا على الوصول إلى واجهات برمجة التطبيقات التي تمنح إمكانية الوصول إلى مادة المفتاح الجزئية فقط، ولكن يمكن أن يتم الخطأ في اعتبارها مفتاحًا كاملاً. في Java، يستخدم Tink RestrictedApi للقيام بذلك، وفي C++ وGolang، يستخدم الرموز المميّزة المشابهة للرموز المميّزة للوصول إلى المفتاح السري.

يتحمّل مستخدمو واجهات برمجة التطبيقات هذه مسؤولية منع كلّ من هجمات إعادة استخدام المفاتيح وحالات عدم التوافق.

غالبًا ما تواجه طُرقًا تحظر "الوصول الجزئي إلى المفاتيح" في سياق تصدير المفاتيح من Tink أو استيرادها إليها. توضّح أفضل الممارسات التالية كيفية العمل بأمان في هذه السيناريوهات.

أفضل الممارسات: التحقّق من جميع المَعلمات عند تصدير المفاتيح

على سبيل المثال، إذا كتبت دالة تُصدِّر مفتاحًا عامًا لبروتوكول HPKE:

طريقة غير مناسبة لتصدير مفتاح عام:

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

هذه مشكلة. بعد استلام المفتاح، تفترض الجهة الخارجية التي تستخدمه بعض الافتراضات حول مَعلمات المفتاح: على سبيل المثال، ستفترض أنّه تم استخدام خوارزمية HPKE AEAD لهذا المفتاح الذي يبلغ طوله 256 بت، وهي 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 يحدِّد الخوارزمية الصحيحة بالكامل ويخزِّن كل البيانات الوصفية مع مادة المفتاح.

راجع المثال التالي:

الاستخدام غير المكتوب:

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، قد يستخدم المُتصل نقطة منحنى التشفير نفسها لتشفير رسالة، ما قد يؤدي بسهولة إلى ظهور ثغرات أمنية.

بدلاً من ذلك، من الأفضل تغيير verifyEcdsaSignature لكي تكون العبارة الأولى هي EcdsaPublicKey. في الواقع، عند قراءة المفتاح من القرص أو الشبكة، يجب تحويله على الفور إلى عنصر 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 عند قراءة الصفيف، ونحدّد تمامًا الخوارزمية التي يجب استخدامها. يقلل هذا النهج من احتمالية حدوث هجمات الخلط بين المفاتيح.