Điều khiển Truy cập

Một mục tiêu của Tink là ngăn chặn các phương pháp không hợp lệ. Điều đặc biệt quan tâm trong phần này là 2 điểm:

  1. Tink khuyến khích việc sử dụng theo cách mà người dùng không thể truy cập vào tài liệu về khoá bí mật. Thay vào đó, bạn nên lưu trữ các khoá bí mật trong KMS bất cứ khi nào có thể bằng một trong những cách được xác định trước giúp Tink hỗ trợ các hệ thống đó.
  2. Tink không khuyến khích người dùng truy cập vào các phần của khoá, vì việc này thường dẫn đến các lỗi về khả năng tương thích.

Trong thực tế, tất nhiên cả hai nguyên tắc này đôi khi phải vi phạm. Để làm được điều này, Tink cung cấp nhiều cơ chế.

Mã truy cập khoá bí mật

Để truy cập vào tài liệu về khoá bí mật, người dùng phải có mã thông báo (thường chỉ là một đối tượng của một lớp nào đó, không có bất kỳ chức năng nào). Mã thông báo thường được cung cấp bằng một phương thức 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. Bên ngoài Google, người đánh giá bảo mật có thể tìm kiếm cơ sở mã của họ để sử dụng chức năng này.

Một tính năng hữu ích của các mã thông báo này là chúng có thể được truyền lại. 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 về khoá bí mật, hàm này yêu cầu đối tượng secretKeyAccess phải khác rỗng và có một mã thông báo SecretKeyAccess thực tế được lưu trữ. Đối với các khoá không có tài liệu bí mật, secretKeyAccess sẽ bị bỏ qua.

Với một hàm như vậy, bạn có thể viết một hàm giúp chuyển đổi tuần tự toàn bộ bộ khoá: String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess secretKeyAccess);

Hàm này gọi serializeKey cho từng khoá trong tập hợp khoá trong 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ự tài liệu khoá bí mật có thể sử dụng null làm đối số thứ hai. Người dùng có nhu cầu chuyển đổi tuần tự tài liệu khoá bí mật cần sử dụng InsecureSecretKeyAccess.get().

Truy cập các phần của khoá

Một lỗi bảo mật tương đối phổ biến là "tấn công sử dụng lại khoá". Điều này có thể xảy ra khi người dùng sử dụng lại, chẳng hạn như mô-đun n và số mũ de của khoá RSA trong hai chế độ cài đặt khác nhau (ví dụ: để tính toán chữ ký và mã hoá)1.

Một lỗi tương đối phổ biến khác khi xử lý khoá mã hoá là chỉ định một phần của khoá rồi "sử dụng" siêu dữ liệu. Ví dụ: giả sử một người dùng muốn xuất khoá công khai RSASSA-PSS từ Tin nhắn để sử dụng với một thư viện khác. Trong Tink, các khoá này có các phần sau:

  • Mô-đun n
  • Số mũ công khai e
  • Thông số kỹ thuật của 2 hàm băm được sử dụng nội bộ
  • Độ dài của dữ liệu ngẫu nhiên được sử dụng nội bộ trong thuật toán.

Khi xuất khoá như vậy, bạn có thể bỏ qua các hàm băm và độ dài dữ liệu ngẫu nhiên. Điều này thường có thể hoạt động hiệu quả, vì các thư viện khác thường không yêu cầu các hàm băm (và ví dụ chỉ giả định SHA256 được sử dụng) và hàm băm được sử dụng trong Tink tình cờ giống như trong thư viện khác (hoặc có thể các hàm băm được chọn để hoạt động cùng với thư viện khác).

Tuy nhiên, việc bỏ qua các hàm băm có thể là một sai lầm có thể gây tốn kém. Để thấy điều này, giả sử sau đó, một khoá mới có hàm băm khác sẽ được thêm vào tập hợp khoá Tink. Giả sử sau đó, khoá được xuất bằng phương thức này và được cung cấp cho một đối tác kinh doanh, đồng thời sử dụng khoá này với thư viện khác. Tink hiện giả định một hàm băm nội bộ khác và không thể xác minh chữ ký.

Trong trường hợp này, hàm xuất khoá sẽ không thành công nếu hàm băm không khớp với dự kiến của thư viện khác: nếu không, khoá đã xuất sẽ vô dụng vì tạo ra các bản mật mã hoặc chữ ký không tương thích.

Để tránh những sai sót như vậy, Tink hạn chế các hàm 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ẫn là khoá đầy đủ. Ví dụ: trong Java, Tink sử dụng RestrictedApi cho việc này.

Khi sử dụng chú thích như vậy, người dùng có trách nhiệm ngăn chặn cả cuộc tấn công sử dụng lại khoá và tình trạng không tương thích.

Phương pháp hay nhất: Sử dụng các đối tượng Tink càng sớm càng tốt khi nhập khoá

Bạn thường gặp nhất các phương thức bị hạn chế "quyền truy cập một phần khoá" khi xuất khoá từ hoặc nhập khoá sang Tink.

Điều này giúp giảm thiểu nguy cơ xảy ra các cuộc tấn công nhầm lẫn quan trọng 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 tài liệu chính.

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

Trường hợp sử dụng không được 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(PublicKeyVerify.class);
    publicKeyVerify.verify(signature, message);
}

Điều này dễ xảy ra lỗi: tại trang web lệnh gọi, bạn rất dễ quên rằng không được dùng cùng một ecPoint với thuật toán khác. Ví dụ: nếu một hàm tương tự có tên là encryptWithECHybridEncrypt tồn tại, thì phương thức gọi có thể sử dụng cùng một điểm đường cong để mã hoá một thông điệp, điều này có thể dễ dàng dẫn đến các lỗ hổng.

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 lập tức sang đố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 là bạn nên xác nhận với khoá đó.

Mã trước đó có thể được cải thiện nhiều hơn nữa. Thay vì truyền EcdsaPublicKey vào, bạn nên truyền KeysetHandle vào. Tính năng này sẽ chuẩn bị mã cho việc xoay khoá mà không cần thực hiện thêm bất kỳ hành động nào. Vì vậy, bạn nên ưu tiên cách này.

Tuy nhiên, các điểm cải tiến sẽ không được thực hiện: tốt hơn hết nên truyền vào đối tượng PublicKeyVerify: đố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 trở nên khá nhỏ và có thể cùng dòng.

Đề xuất: Khi tài liệu chính đượ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.

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 tôi sẽ ngay lập tức chuyển đổi mảng byte thành đối tượng Tink khi đối tượng đó được đọc và chúng tôi chỉ định đầy đủ thuật toán nào sẽ được sử dụng. Phương pháp này giúp giảm thiểu xác suất xảy ra các cuộc tấn công gây nhầm lẫn quan trọng.

Phương pháp hay nhất: Xác minh tất cả thông 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 xuất khoá công khai không phù hợp:

/** Provide the key to our users which do not 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á đó: chẳng hạn như họ sẽ giả định rằng thuật toán HPKE AEAD dùng cho khoá 256 bit này là AES-GCM, v.v.

Đề xuất: Xác minh rằng các thông số này đúng với mong đợi của bạn khi xuất khoá.

Cách tốt hơn để xuất khoá công khai:

/** Provide the key to our users which do not 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 do not add a Tink style prefix
    if (!key.getParameters().getVariant().equals(HpkeParameters.Variant.NO_PREFIX)) {
        throw new IllegalArgumentException("Bad parameters");
    }
    return key.getPublicKeyBytes().toByteArray();
}