액세스 제어

Tink의 목표 중 하나는 잘못된 관행을 방지하는 것입니다. 이 섹션에서 특히 흥미로운 내용은 두 가지입니다.

  1. Tink는 사용자가 보안 비밀 키 자료에 액세스할 수 없는 방식으로 사용을 권장합니다. 대신 보안 비밀 키는 가능하면 Tink에서 이러한 시스템을 지원하는 사전 정의된 방법 중 하나를 사용하여 KMS에 저장해야 합니다.
  2. Tink는 사용자가 키의 일부에 액세스하지 못하도록 하며, 액세스 시 호환성 버그가 발생하기 때문입니다.

물론 실제로는 이 두 가지 원칙을 모두 위반해야 하는 경우가 있습니다. 이를 위해 Tink는 다양한 메커니즘을 제공합니다.

보안 비밀 키 액세스 토큰

보안 비밀 키 자료에 액세스하려면 사용자는 토큰 (일반적으로 기능이 없는 특정 클래스의 객체)이 있어야 합니다. 토큰은 일반적으로 InsecureSecretKeyAccess.get()와 같은 메서드에서 제공됩니다. Google 내에서 사용자는 Bazel BUILD 공개 상태를 사용하여 이 함수를 사용할 수 없습니다. Google 외부에서 보안 검토자는 코드베이스를 검색하여 이 함수를 사용할 수 있습니다.

이러한 토큰의 한 가지 유용한 기능은 토큰을 전달할 수 있다는 것입니다. 예를 들어 임의의 Tink 키를 직렬화하는 함수가 있다고 가정해 보겠습니다.

String serializeKey(Key key, @Nullable SecretKeyAccess secretKeyAccess);

보안 비밀 키 자료가 있는 키의 경우 이 함수를 사용하려면 secretKeyAccess 객체가 null이 아니어야 하고 실제 SecretKeyAccess 토큰이 저장되어 있어야 합니다. 보안 비밀 자료가 없는 키의 경우 secretKeyAccess가 무시됩니다.

이러한 함수가 주어지면 전체 키 세트를 직렬화하는 함수를 작성할 수 있습니다. String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccesssecretKeyAccess);

이 함수는 내부적으로 키 세트의 각 키에 대해 serializeKey를 호출하고 지정된 secretKeyAccess를 기본 함수에 전달합니다. 그런 다음 보안 비밀 키 자료를 직렬화하지 않고도 serializeKeyset를 호출하는 사용자는 null를 두 번째 인수로 사용할 수 있습니다. 보안 비밀 키 자료를 직렬화해야 하는 사용자는 InsecureSecretKeyAccess.get()를 사용해야 합니다.

키 부분에 대한 액세스

비교적 일반적인 보안 버그는 '키 재사용 공격'입니다. 이는 사용자가 두 가지 다른 설정 (예: 서명 및 암호화 계산)에서 RSA 키의 모듈러스 n과 지수 de를 재사용하는 경우에 발생할 수 있습니다1.

암호화 키를 처리할 때 상대적으로 흔히 범하는 또 다른 실수는 키의 일부를 지정한 다음 메타데이터를 '가정'하는 것입니다. 예를 들어 사용자가 다른 라이브러리에서 사용하기 위해 Tink에서 RSASSA-PSS 공개 키를 내보내려고 한다고 가정해 보겠습니다. Tink에서 이러한 키는 다음과 같은 부분으로 구성됩니다.

  • 계수 n
  • 공개 지수 e
  • 내부적으로 사용되는 두 해시 함수의 사양
  • 알고리즘에서 내부적으로 사용되는 솔트의 길이입니다.

이러한 키를 내보낼 때 해시 함수와 솔트 길이를 무시할 수 있습니다. 이는 다른 라이브러리에서 해시 함수를 요청하지 않는 경우가 많으므로 (예를 들어 SHA256이 사용된다고 가정) Tink에 사용된 해시 함수가 다른 라이브러리에서와 동시에 동일합니다 (또는 다른 라이브러리와 함께 작동하도록 해시 함수가 특별히 선택되었을 수 있음).

그럼에도 불구하고 해시 함수를 무시하는 것은 많은 비용이 드는 실수일 수 있습니다. 이를 확인하기 위해 나중에 다른 해시 함수가 포함된 새 키가 Tink 키 세트에 추가된다고 가정해 보겠습니다. 그러면 키를 메서드와 함께 내보내고 비즈니스 파트너에게 제공하여 다른 라이브러리와 함께 사용한다고 가정해 보겠습니다. 이제 Tink는 다른 내부 해시 함수를 가정하여 서명을 확인할 수 없습니다.

이 경우 해시 함수가 다른 라이브러리에서 예상하는 것과 일치하지 않으면 키를 내보내는 함수가 실패합니다. 그렇지 않으면 내보낸 키가 호환되지 않는 암호문이나 서명을 만들어 쓸모가 없습니다.

이러한 실수를 방지하기 위해 Tink는 부분적인 키 자료에 대한 액세스 권한을 부여하는 함수를 제한합니다. 이 함수는 전체 키로 오해할 수 있습니다. 예를 들어 자바 Tink에서는 이를 위해 RestrictedApi를 사용합니다.

이러한 주석을 사용하는 사용자는 키 재사용 공격과 비호환성을 모두 방지해야 합니다.

권장사항: 키 가져오기 시 가능한 한 빨리 Tink 객체 사용

Tink에서 키를 내보내거나 가져올 때 '부분 키 액세스'로 제한되는 메서드를 가장 일반적으로 접하게 됩니다.

Tink Key 객체는 올바른 알고리즘을 완전히 지정하고 모든 메타데이터를 키 자료와 함께 저장하기 때문에 이렇게 하면 키 혼동 공격의 위험이 최소화됩니다.

다음 예를 참고하세요.

입력되지 않은 사용:

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);
}

이 경우 오류가 발생하기 쉽습니다. 호출 사이트에서 다른 알고리즘과 함께 동일한 ecPoint를 사용하면 안 된다는 점을 잊어버리기 쉽습니다. 예를 들어 encryptWithECHybridEncrypt이라는 유사한 함수가 있는 경우 호출자는 동일한 곡선 포인트를 사용하여 메시지를 암호화할 수 있으며 이는 취약점으로 이어질 수 있습니다.

대신 첫 번째 인수가 EcdsaPublicKey가 되도록 verifyEcdsaSignature를 변경하는 것이 좋습니다. 실제로, 디스크나 네트워크에서 키를 읽을 때마다 즉시 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 객체로 변환하고 사용해야 할 알고리즘을 완전히 지정합니다. 이 접근 방식은 주요 혼동 공격의 가능성을 최소화합니다.

권장사항: 키 내보내기에서 모든 매개변수 확인

예를 들어 HPKE 공개 키를 내보내는 함수를 작성하는 경우:

공개 키를 내보내는 잘못된 방법:

/** Provide the key to our users which do not have Tink. */
byte[] exportTinkHpkeKey(HpkePublicKey key) {
    return key.getPublicKeyBytes().toByteArray();
}

이는 문제가 있습니다. 키를 받은 후 키를 사용하는 서드 파티는 키의 매개변수를 일부 가정합니다. 예를 들어 이 키 256비트에 사용된 HPKE AEAD 알고리즘이 AES-GCM이라고 가정합니다.

권장사항: 키가 키 내보내기에서 예상하는 매개변수와 일치하는지 확인하세요.

공개 키를 내보내는 더 좋은 방법:

/** 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();
}