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