ส่งออกเนื้อหาคีย์แบบเป็นโปรแกรม

Tink ไม่สนับสนุนแนวทางปฏิบัติที่ไม่ถูกต้องเกี่ยวกับคีย์ เช่น

  • สิทธิ์เข้าถึงเนื้อหาคีย์ลับของผู้ใช้ – ควรจัดเก็บคีย์ลับใน KMS แทนหากเป็นไปได้ โดยใช้วิธีที่ Tink รองรับระบบดังกล่าว
  • ผู้ใช้เข้าถึงบางส่วนของคีย์ ซึ่งมักทำให้เกิดข้อบกพร่องด้านความเข้ากันได้

ในความเป็นจริง มีบางกรณีที่จำเป็นต้องละเมิดหลักการเหล่านี้ 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 เป็นอาร์กิวเมนต์ที่ 2 ได้ ผู้ใช้ที่ต้องการจัดรูปแบบข้อมูลคีย์ลับควรใช้ InsecureSecretKeyAccess.get()

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

คีย์ Tink ไม่ได้มีแค่เนื้อหาคีย์ดิบ แต่ยังมีข้อมูลเมตาที่ระบุวิธีใช้คีย์ด้วย (ซึ่งไม่ควรใช้คีย์ในลักษณะอื่นใด) ตัวอย่างเช่น คีย์ RSA SSA PSS ใน Tink จะระบุว่าคีย์ RSA นี้ใช้ได้กับอัลกอริทึมลายเซ็น PSS โดยใช้ฟังก์ชันแฮชที่ระบุและความยาว Salt ที่ระบุเท่านั้น

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

เมื่อแปลงคีย์ Tink ในรูปแบบอื่น คุณต้องตรวจสอบว่าข้อมูลเมตาของคีย์ Tink ตรงกับข้อมูลเมตา (ที่ระบุโดยนัย) ของรูปแบบคีย์อื่น หากไม่ตรงกัน Conversion จะดำเนินการไม่สำเร็จ

เนื่องจากการตรวจสอบเหล่านี้มักขาดหายไปหรือไม่สมบูรณ์ Tink จึงจำกัดการเข้าถึง API ที่ให้สิทธิ์เข้าถึงข้อมูลคีย์เพียงบางส่วน แต่อาจทำให้เข้าใจผิดว่าเป็นคีย์แบบเต็ม ใน Java Tink จะใช้ RestrictedApi สำหรับการทำงานนี้ใน C++ และ Golang และใช้โทเค็นที่คล้ายกับโทเค็นเพื่อการเข้าถึงคีย์ลับ

ผู้ใช้ของ API เหล่านี้มีหน้าที่ในการป้องกันทั้งการโจมตีคีย์ซ้ำและความไม่เข้ากัน

คุณมักจะพบวิธีการที่จำกัด "สิทธิ์เข้าถึงคีย์บางส่วน" ในบริบทของการส่งออกคีย์จากหรือนำเข้าคีย์ไปยัง 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 ทันที เนื่องจากตอนนี้คุณทราบแล้วว่าจะใช้คีย์ในลักษณะใด จึงควรใช้รูปแบบนั้น

โค้ดที่อยู่ก่อนหน้าจะได้รับการปรับปรุงมากกว่านี้อีก การใส่ KeysetHandle แทน EcdsaPublicKey นั้นดีกว่า และเตรียมโค้ดสำหรับการหมุนเวียนคีย์โดยไม่ต้องทำอะไรเพิ่มเติม คุณจึงควรใช้วิธีนี้

อย่างไรก็ตาม การปรับปรุงยังไม่เสร็จสิ้น: การใส่ออบเจ็กต์ 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 ทันทีที่อ่าน และระบุอัลกอริทึมที่ควรใช้อย่างครบถ้วน แนวทางนี้จะช่วยลดโอกาสที่จะถูกโจมตีด้วยคีย์ที่คล้ายกัน