การควบคุมการเข้าถึง

เป้าหมายหนึ่งของ Tink คือการต่อต้านการกระทำที่ไม่เหมาะสม สิ่งที่สนใจในส่วนนี้คือ 2 ประเด็น ได้แก่

  1. Tink ส่งเสริมการใช้งานในลักษณะที่ทำให้ผู้ใช้ไม่สามารถเข้าถึงเนื้อหาคีย์ลับได้ แต่ควรจัดเก็บคีย์ลับใน KMS แทนทุกครั้งที่ทำได้ โดยใช้วิธีการที่ Tink รองรับไว้อย่างใดอย่างหนึ่งที่กำหนดไว้ล่วงหน้า
  2. Tink ไม่สนับสนุนให้ผู้ใช้เข้าถึงส่วนต่างๆ ของคีย์ เนื่องจากการดำเนินการดังกล่าวมักทำให้มีข้อบกพร่องในความเข้ากันได้

ในทางปฏิบัติ แน่นอนว่าต้องมีการละเมิดหลักการทั้ง 2 ข้อนี้บ้างในบางครั้ง สำหรับกรณีนี้ Tink มีกลไกที่แตกต่างกัน

โทเค็นการเข้าถึงคีย์ลับ

ผู้ใช้ต้องมีโทเค็น (ซึ่งมักจะเป็นออบเจ็กต์ของบางคลาส โดยไม่มีฟังก์ชันการทำงาน) เพื่อเข้าถึงเนื้อหาคีย์ลับ โดยปกติแล้ว โทเค็นจะระบุโดยเมธอด เช่น InsecureSecretKeyAccess.get() ภายใน Google ผู้ใช้จะไม่สามารถใช้ฟังก์ชันนี้โดยใช้การเปิดเผยของรุ่น Bazel ภายนอก Google ผู้ตรวจสอบด้านความปลอดภัยสามารถค้นหาฐานของโค้ดเพื่อใช้ฟังก์ชันนี้ได้

คุณลักษณะที่มีประโยชน์อย่างหนึ่งของโทเค็นเหล่านี้คือสามารถส่งต่อได้ เช่น สมมติว่าคุณมีฟังก์ชันที่เรียงอันดับคีย์ Tink ที่กำหนดเอง ดังนี้

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

สำหรับคีย์ที่มีเนื้อหาคีย์ลับ ฟังก์ชันนี้กำหนดให้ออบเจ็กต์ secretKeyAccess ต้องไม่เป็น null และมีโทเค็น SecretKeyAccess จริงที่จัดเก็บไว้ ระบบจะละเว้น secretKeyAccess สำหรับคีย์ที่ไม่มีเนื้อหาลับใดๆ

ด้วยฟังก์ชันเช่นนี้ เป็นไปได้ที่จะเขียนฟังก์ชันที่ทำให้ชุดคีย์ทั้งหมดเป็นอนุกรม: สตริงerializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

ฟังก์ชันนี้เรียกใช้ serializeKey สำหรับแต่ละคีย์ในชุดคีย์ภายใน และส่งต่อ secretKeyAccess ที่กำหนดไปยังฟังก์ชันที่สำคัญ จากนั้นผู้ใช้ที่เรียกใช้ serializeKeyset โดยไม่ต้องเรียงลําดับเนื้อหาคีย์ลับจะใช้ null เป็นอาร์กิวเมนต์ที่ 2 ได้ ผู้ใช้ที่จำเป็นต้องต่อเนื่องเนื้อหาคีย์ลับต้องใช้ InsecureSecretKeyAccess.get()

การเข้าถึงส่วนต่างๆ ของคีย์

ข้อบกพร่องด้านความปลอดภัยที่พบได้ทั่วไปคือ "การโจมตีที่สำคัญที่ใช้ซ้ำ" ข้อผิดพลาดนี้อาจเกิดขึ้นเมื่อผู้ใช้ใช้โมดูลัส n และเลขชี้กำลัง d และ e ของคีย์ RSA ซ้ำในการตั้งค่า 2 แบบ (เช่น เพื่อคำนวณลายเซ็นและการเข้ารหัส)1

ความผิดพลาดที่พบได้บ่อยอีกอย่างหนึ่งเมื่อจัดการกับคีย์การเข้ารหัสคือการระบุส่วนของคีย์ จากนั้น "สมมติ" ข้อมูลเมตา ตัวอย่างเช่น สมมติว่าผู้ใช้ต้องการส่งออกคีย์สาธารณะ RSASSA-PSS จาก Tink เพื่อใช้กับไลบรารีอื่น ใน Tink คีย์เหล่านี้จะมีส่วนต่อไปนี้

  • โมดูลัส n
  • เลขชี้กำลังสาธารณะ e
  • ข้อกำหนดของฟังก์ชันแฮช 2 แบบที่ใช้ภายใน
  • ความยาวของเกลือที่ใช้ภายในอัลกอริทึม

เมื่อคุณส่งออกคีย์ดังกล่าว คุณอาจไม่สนใจฟังก์ชันแฮชและความยาวของ Salt ซึ่งมักจะใช้ได้ดี เนื่องจากไลบรารีอื่นๆ จะไม่ขอฟังก์ชันแฮช (เช่น สมมติว่ามีการใช้ 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();
}