کنترل دسترسی

یکی از اهداف تینک جلوگیری از اعمال بد است. در این بخش دو نکته جالب توجه است:

  1. Tink استفاده را به گونه ای تشویق می کند که کاربران نتوانند به مطالب کلید سری دسترسی پیدا کنند. درعوض، کلیدهای مخفی باید تا حد امکان در یک KMS با استفاده از یکی از روش های از پیش تعریف شده ای که Tink از چنین سیستم هایی پشتیبانی می کند، ذخیره شود.
  2. 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() استفاده کنند.

دسترسی به قطعات یک کلید

یک اشکال امنیتی نسبتاً رایج "حمله استفاده مجدد از کلید" است. این می تواند زمانی رخ دهد که کاربران به عنوان مثال مدول n و توان d و e یک کلید RSA را در دو تنظیمات مختلف (مثلاً برای محاسبه امضا و رمزگذاری) مجدداً استفاده کنند.

یکی دیگر از اشتباهات نسبتا رایج دیگر هنگام برخورد با کلیدهای رمزنگاری این است که بخشی از کلید را مشخص می کنیم و سپس ابرداده را "فرض" می کنیم. برای مثال، فرض کنید کاربری می‌خواهد یک کلید عمومی RSASSA-PSS را از Tink برای استفاده در کتابخانه دیگری صادر کند. در Tink، این کلیدها دارای قسمت های زیر هستند:

  • مدول n
  • نماینده عمومی e
  • مشخصات دو تابع هش مورد استفاده داخلی
  • طول نمک استفاده شده در داخل الگوریتم.

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

با این وجود، نادیده گرفتن توابع هش یک اشتباه بالقوه گران خواهد بود. برای مشاهده این موضوع، فرض کنید بعداً یک کلید جدید با عملکرد هش متفاوت به مجموعه کلیدهای Tink اضافه شده است. فرض کنید که پس از آن، کلید با روش صادر شده و به یک شریک تجاری داده می شود، که از آن با کتابخانه دیگر استفاده می کند. Tink اکنون تابع هش داخلی متفاوتی را در نظر می گیرد و نمی تواند امضا را تأیید کند.

در این حالت، اگر تابع هش با آنچه کتابخانه دیگر انتظار دارد مطابقت نداشته باشد، تابع صادرکننده کلید باید از کار بیفتد: در غیر این صورت، کلید صادر شده بی فایده است زیرا متن های رمزی یا امضاهای ناسازگار ایجاد می کند.

برای جلوگیری از چنین اشتباهاتی، Tink توابعی را محدود می کند که به مواد کلیدی دسترسی دارند که فقط جزئی است، اما ممکن است به عنوان یک کلید کامل اشتباه گرفته شود. به عنوان مثال، در جاوا 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();
}