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

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

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

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

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

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

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

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