Tink ไม่สนับสนุนแนวทางปฏิบัติที่ไม่ถูกต้องเกี่ยวกับคีย์ เช่น
- สิทธิ์เข้าถึงเนื้อหาคีย์ลับของผู้ใช้ – ควรจัดเก็บคีย์ลับใน KMS แทนหากเป็นไปได้ โดยใช้วิธีที่ Tink รองรับระบบดังกล่าว
- ผู้ใช้เข้าถึงบางส่วนของคีย์ ซึ่งมักทำให้เกิดข้อบกพร่องด้านความเข้ากันได้
แต่ในทางปฏิบัติ ก็มีบางกรณีที่จำเป็นต้องละเมิดหลักการเหล่านี้ Tink มีกลไกที่ทำให้สามารถทำเช่นนั้นได้อย่างปลอดภัยตามที่อธิบายไว้ในส่วนต่อไปนี้
โทเค็นการเข้าถึงคีย์ข้อมูลลับ
ผู้ใช้ต้องมีโทเค็น (ซึ่งมักจะเป็นออบเจ็กต์ของคลาสใดคลาสหนึ่งโดยไม่มีฟังก์ชันการทำงานใดๆ) จึงจะเข้าถึงข้อมูลคีย์ลับได้ โดยปกติแล้วโทเค็นจะมาจากวิธีการต่างๆ เช่น InsecureSecretKeyAccess.get()
ภายใน Google ระบบจะป้องกันไม่ให้ผู้ใช้ใช้ฟังก์ชันนี้โดยใช้ระดับการเข้าถึง BUILD ของ Bazel ผู้ตรวจสอบความปลอดภัยภายนอก 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
ทันที เนื่องจากตอนนี้คุณทราบแล้วว่าจะใช้คีย์ในลักษณะใด จึงควรใช้รูปแบบนั้น
รหัสก่อนหน้ายังปรับปรุงให้ดียิ่งขึ้นได้ แทนที่จะส่งผ่านใน 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 ทันทีที่อ่าน และระบุอัลกอริทึมที่ควรใช้อย่างครบถ้วน แนวทางนี้จะช่วยลดโอกาสที่จะถูกโจมตีด้วยคีย์ที่คล้ายกัน