Xuất tài liệu quan trọng theo phương thức lập trình

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 đó, 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 được xác định trước 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ư được 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ể truyề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 có nội dung 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 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 khoá RSA này chỉ có thể được sử dụng với thuật toán chữ ký PSS bằng hàm băm được chỉ định và độ dài muối đượ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ỉ 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 trường hợp 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 đúng để 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 rằng 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 gây nhầm lẫn khoá, vì đối tượng Khoá Tink 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 tài liệu khoá.

Hãy xem ví dụ sau đây:

Cách sử dụng không 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á một 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, khoá đó phải được chuyển đổi ngay thành đối tượng EcdsaPublicKey: tại thời điểm này, bạn đã biết cách sử dụng khoá, vì vậy, tốt nhất là bạn nên cam kết với khoá đó.

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 vào 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, bạn vẫn chưa hoàn tất việc cải thiện: tốt hơn hết là bạn nên truyền vào đối tượng PublicKeyVerify: đối tượng này là đủ cho hàm này, vì vậy, việc truyền vào đối tượng PublicKeyVerify có thể làm tăng số lượng 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 tài liệu khoá được đọc từ ổ đĩa hoặc mạng lần đầu tiên, hãy tạo các đối tượng Tink tương ứng càng sớm càng tốt.

Cách 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 giúp giảm thiểu khả năng bị tấn công gây nhầm lẫn khoá.