صادر کردن برنامه‌نویسی‌شده مطالب کلیدی

Tink از روش‌های بد مربوط به کلیدها جلوگیری می‌کند، مانند:

  • دسترسی کاربر به مواد کلید مخفی – درعوض، کلیدهای مخفی باید درصورت امکان بااستفاده از یکی از روش‌های ازپیش‌تعریف‌شده‌ای که Tink از چنین سیستم‌هایی پشتیبانی می‌کند، در KMS ذخیره شوند.
  • دسترسی کاربر به بخش‌هایی از کلیدها – این کار اغلب منجر به اشکالات سازگاری می‌شود.

در واقعیت، مواردی وجود دارد که نقض این اصول را ضروری می‌کند. 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() استفاده کنند.

دسترسی به بخش‌هایی از کلید

کلیدهای Tink نه‌تنها حاوی مواد کلید خام هستند، بلکه فراداده‌ای نیز دارند که مشخص می‌کند کلید چگونه باید استفاده شود (و به‌نوبه خود، اینکه نباید به هیچ روش دیگری استفاده شود). برای مثال، کلید RSA SSA PSS در Tink مشخص می‌کند که این کلید RSA فقط می‌تواند با الگوریتم امضای PSS بااستفاده از تابع چکیده‌ساز مشخص‌شده و طول سالت مشخص‌شده استفاده شود.

گاهی اوقات لازم است کلید Tink به قالب‌های دیگری تبدیل شود که ممکن است همه این فراداده‌ها را به‌طور صریح مشخص نکنند. این معمولاً به این معنی است که فراداده باید هنگام استفاده از کلید ارائه شود. به‌عبارت دیگر، با فرض اینکه کلید همیشه با همان الگوریتم استفاده می‌شود، چنین کلیدی همچنان به‌طور ضمنی همان فراداده را دارد، فقط در مکان دیگری ذخیره می‌شود.

وقتی کلید Tink را به قالب دیگری تبدیل می‌کنید، باید مطمئن شوید که فراداده کلید Tink با فراداده (به‌طور ضمنی مشخص‌شده) قالب کلید دیگر مطابقت داشته باشد. اگر مطابقت نداشته باشد، تبدیل باید ناموفق باشد.

ازآنجایی‌که این بررسی‌ها اغلب وجود ندارند یا ناقص هستند، Tink دسترسی به میاناهای برنامه‌سازی کاربردی را که به مواد کلیدی دسترسی می‌دهند محدود می‌کند، که این دسترسی فقط جزئی است، اما ممکن است به‌عنوان کلید کامل اشتباه گرفته شود. در Java،‏ Tink از RestrictedApi برای این کار استفاده می‌کند، در C++‎ و Golang، از کدهای دسترسی مشابه کدهای دسترسی کلید خصوصی استفاده می‌کند.

کاربران این APIها مسئول جلوگیری از حملات استفاده مجدد از کلید و ناسازگاری‌ها هستند.

معمولاً با روش‌هایی مواجه می‌شوید که «دسترسی جزئی به کلید» را در زمینه صادر کردن کلیدها از Tink یا وارد کردن کلیدها به Tink محدود می‌کنند. روال‌های مطلوب زیر نحوه عملکرد ایمن در این سناریوها را توضیح می‌دهد.

روال مطلوب: درستی‌سنجی همه پارامترها در صادر کردن کلید

برای مثال، اگر تابعی بنویسید که کلید عمومی HPKE را صادر می‌کند:

روش نادرست برای صادر کردن کلید عمومی:

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

این مشکل‌ساز است. پس‌از دریافت کلید، شخص ثالثی که از آن استفاده می‌کند، فرضیاتی درباره پارامترهای کلید مطرح می‌کند: برای مثال، فرض می‌کند که الگوریتم HPKE AEAD استفاده‌شده برای این کلید ۲۵۶ بیتی 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 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(TinkConfig2026.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 تبدیل می‌کنیم وقتی که خوانده می‌شود، و ما به‌طور کامل مشخص می‌کنیم که چه الگوریتمی باید استفاده شود. این رویکرد احتمال حملات سردرگمی کلید را به‌حداقل می‌رساند.