التحكم في الدخول

أحد أهداف Tink هو الحد من الممارسات السيئة. من النقاط ذات الأهمية الخاصة في هذا القسم نقطتان:

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

من الناحية العملية، يجب بالطبع انتهاك هذين المبدأَين في بعض الأحيان. لهذا، يوفر Tink آليات مختلفة.

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

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

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

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

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

بناءً على هذه الدالة، يمكن كتابة دالة تنشئ تسلسلاً لمجموعة مفاتيح كاملة: String seriesizeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

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

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

ويعد أحد أخطاء الأمان الشائعة نسبيًا "هجوم إعادة استخدام المفتاح". ويمكن أن يحدث ذلك عندما يعيد المستخدمون استخدام المعامل n والأُسَّين d وe لمفتاح RSA مثلاً في إعدادَين مختلفَين (على سبيل المثال، لاحتساب التوقيعات والتشفيرات)1.

من الأخطاء الأخرى الشائعة نسبيًا عند التعامل مع مفاتيح التشفير تحديد جزء من المفتاح، ثم "افتراض" البيانات الوصفية. على سبيل المثال، لنفترض أنّ أحد المستخدمين يريد تصدير مفتاح عام RSASSA-PSS من Tink لاستخدامه مع مكتبة مختلفة. في Tink، تحتوي هذه المفاتيح على الأجزاء التالية:

  • المعامل n
  • الأس العام e
  • تحديد دالتَي التجزئة المستخدمتَين داخليًا
  • طول الملح المستخدَم داخليًا في الخوارزمية.

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

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

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

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

طريقة تصدير مفتاح عام غير صحيحة:

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

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