Tink không khuyến khích các phương pháp không tốt liên quan đến khoá, chẳng hạn như:
- Quyền truy cập của người dùng vào tài liệu khoá bí mật – Thay vào đó, các khoá bí mật phải được lưu trữ trong KMS bất cứ khi nào có thể bằng cách sử dụng một trong các cách thức đã xác định sẵn mà Tink hỗ trợ các hệ thống như vậy.
- Quyền truy cập của người dùng vào một số phần của khoá – Việc này thường dẫn đến lỗi về khả năng tương thích.
Trên thực tế, có một số trường hợp cần phải vi phạm các nguyên tắc này. Tink cung cấp các cơ chế để có thể thực hiện việc này một cách an toàn như mô tả trong các phần sau.
Mã truy cập khoá bí mật
Để truy cập vào tài liệu khoá bí mật, người dùng phải có mã thông báo (thường là một đối tượng của một số lớp, không có chức năng nào). Mã thông báo này thường do một phương thức cung cấp, chẳng hạn như InsecureSecretKeyAccess.get()
. Trong Google, người dùng không được sử dụng hàm này bằng cách sử dụng chế độ hiển thị Bazel BUILD. Ngoài Google, người đánh giá bảo mật có thể tìm kiếm cơ sở mã của họ để biết cách sử dụng hàm này.
Một tính năng hữu ích của các mã thông báo này là bạn có thể chuyển chúng. Ví dụ: giả sử bạn có một hàm chuyển đổi tuần tự một khoá Tink tuỳ ý:
String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);
Đối với các khoá có tài liệu khoá bí mật, hàm này yêu cầu đối tượng secretKeyAccess
không được rỗng và có mã thông báo SecretKeyAccess
thực tế được lưu trữ. Đối với các khoá không chứa bất kỳ tài liệu bí mật nào, secretKeyAccess
sẽ bị bỏ qua.
Với một hàm như vậy, bạn có thể viết một hàm chuyển đổi tuần tự toàn bộ tập hợp khoá:
String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess
secretKeyAccess);
Hàm này gọi serializeKey
cho mỗi khoá trong tập hợp khoá nội bộ và truyền secretKeyAccess
đã cho đến hàm cơ bản. Sau đó, những người dùng gọi serializeKeyset
mà không cần chuyển đổi tuần tự nội dung khoá bí mật có thể sử dụng null
làm đối số thứ hai. Những người dùng cần chuyển đổi tuần tự tài liệu khoá bí mật nên sử dụng InsecureSecretKeyAccess.get()
.
Quyền truy cập vào các phần của một khoá
Khoá Tink không chỉ chứa nội dung khoá thô mà còn chứa siêu dữ liệu chỉ định cách sử dụng khoá (và ngược lại, không được sử dụng khoá theo bất kỳ cách nào khác). Ví dụ: khoá RSA SSA PSS trong Tink chỉ định rằng bạn chỉ có thể dùng khoá RSA này với thuật toán chữ ký PSS bằng cách sử dụng hàm băm được chỉ định và độ dài dữ liệu ngẫu nhiên được chỉ định.
Đôi khi, bạn cần chuyển đổi khoá Tink sang các định dạng khác nhau có thể không chỉ định rõ ràng tất cả siêu dữ liệu này. Điều này thường có nghĩa là bạn cần cung cấp siêu dữ liệu khi sử dụng khoá. Nói cách khác, giả sử khoá luôn được sử dụng với cùng một thuật toán, thì khoá đó vẫn ngầm ẩn có cùng siêu dữ liệu, chỉ là được lưu trữ ở một vị trí khác.
Khi chuyển đổi khoá Tink sang một định dạng khác, bạn cần đảm bảo rằng siêu dữ liệu của khoá Tink khớp với siêu dữ liệu (được chỉ định ngầm) của định dạng khoá khác. Nếu không khớp, lượt chuyển đổi sẽ không thành công.
Vì các bước kiểm tra này thường bị thiếu hoặc không đầy đủ, nên Tink hạn chế quyền truy cập vào các API cấp quyền truy cập vào tài liệu khoá chỉ là một phần nhưng có thể bị nhầm là khoá đầy đủ. Trong Java, Tink sử dụng RestrictedApi cho việc này, trong C++ và Golang, Tink sử dụng mã thông báo tương tự như mã thông báo truy cập khoá bí mật.
Người dùng các API này chịu trách nhiệm ngăn chặn cả các cuộc tấn công sử dụng lại khoá và các trường hợp không tương thích.
Thông thường, bạn sẽ gặp các phương thức hạn chế "quyền truy cập một phần vào khoá" trong bối cảnh xuất khoá từ hoặc nhập khoá vào Tink. Các phương pháp hay nhất sau đây giải thích cách vận hành an toàn trong những tình huống này.
Phương pháp hay nhất: Xác minh tất cả tham số khi xuất khoá
Ví dụ: nếu bạn viết một hàm xuất khoá công khai HPKE:
Cách không nên xuất khoá công khai:
/** Provide the key to our users which don't have Tink. */ byte[] exportTinkHpkeKey(HpkePublicKey key) { return key.getPublicKeyBytes().toByteArray(); }
Đây là vấn đề. Sau khi nhận được khoá, bên thứ ba sử dụng khoá đó sẽ đưa ra một số giả định về các tham số của khoá: ví dụ: giả định rằng thuật toán HPKE AEAD dùng cho khoá 256 bit này là AES-GCM.
Đề xuất: Xác minh các tham số là những gì bạn mong đợi khi xuất khoá.
Cách tốt hơn để xuất khoá công khai:
/** 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(); }
Phương pháp hay nhất: Sử dụng các đối tượng Tink sớm nhất có thể khi nhập khoá
Điều này giúp giảm thiểu nguy cơ bị tấn công nhầm lẫn khoá vì đối tượng Tink Key chỉ định đầy đủ thuật toán chính xác và lưu trữ tất cả siêu dữ liệu cùng với nội dung khoá.
Hãy xem ví dụ sau đây:
Cách sử dụng không phải nhập:
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); }
Điều này dễ xảy ra lỗi: tại vị trí gọi, bạn rất dễ quên rằng bạn không bao giờ nên sử dụng cùng một ecPoint
với một thuật toán khác. Ví dụ: nếu có một hàm tương tự có tên là encryptWithECHybridEncrypt
, thì phương thức gọi có thể sử dụng cùng một điểm cong để mã hoá thông báo, điều này có thể dễ dàng dẫn đến các lỗ hổng bảo mật.
Thay vào đó, bạn nên thay đổi verifyEcdsaSignature
để đối số đầu tiên là EcdsaPublicKey
. Trên thực tế, bất cứ khi nào khoá được đọc từ ổ đĩa hoặc mạng, thì khoá đó phải được chuyển đổi ngay lập tức thành đối tượng EcdsaPublicKey
: tại thời điểm này, bạn đã biết khoá được sử dụng theo cách nào, vì vậy, tốt nhất bạn nên xác nhận.
Bạn có thể cải thiện mã trước đó hơn nữa. Thay vì truyền vào EcdsaPublicKey
, bạn nên truyền KeysetHandle
. Phương thức này chuẩn bị mã cho việc xoay khoá mà không cần làm gì thêm. Vì vậy, bạn nên sử dụng phương thức này.
Tuy nhiên, việc cải thiện vẫn chưa được cải thiện: tốt hơn hết là bạn nên chuyển đối tượng PublicKeyVerify
vào: đối tượng này là đủ, vì vậy, việc truyền đối tượng PublicKeyVerify
có thể làm tăng số vị trí có thể sử dụng hàm này. Tuy nhiên, tại thời điểm này, hàm này trở nên khá tầm thường và có thể được nội tuyến.
Đề xuất: Khi lần đầu đọc tài liệu khoá từ ổ đĩa hoặc mạng, hãy tạo các đối tượng Tink tương ứng càng sớm càng tốt.
Trường hợp sử dụng đã nhập:
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(); }
Khi sử dụng mã như vậy, chúng ta sẽ chuyển đổi ngay mảng byte thành đối tượng Tink khi đọc và chỉ định đầy đủ thuật toán cần sử dụng. Phương pháp này sẽ giảm thiểu xác suất xảy ra các cuộc tấn công nhầm lẫn khoá.