لا ينصح فريق 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 عند قراءتها، ونحدد تمامًا الخوارزمية التي يجب استخدامها. يقلل هذا النهج من احتمالية حدوث هجمات الخلط بين المفاتيح.