프로그래매틱 방식으로 키 자료 내보내기

Tink는 다음과 같은 키와 관련된 잘못된 관행을 권장하지 않습니다.

  • 보안 비밀 키 자료에 대한 사용자 액세스 – 가능한 경우 Tink가 이러한 시스템을 지원하는 사전 정의된 방법 중 하나를 사용하여 보안 비밀 키를 KMS에 저장해야 합니다.
  • 키의 일부에 대한 사용자 액세스: 이렇게 하면 호환성 버그가 발생하는 경우가 많습니다.

실제로는 이러한 원칙을 위반해야 하는 경우가 있습니다. Tink는 다음 섹션에 설명된 대로 안전하게 이를 수행할 수 있는 메커니즘을 제공합니다.

보안 비밀 키 액세스 토큰

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

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

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

보안 키 자료가 있는 키의 경우 이 함수에는 secretKeyAccess 객체가 null이 아니어야 하며 저장된 실제 SecretKeyAccess 토큰이 있어야 합니다. 보안 자료가 없는 키의 경우 secretKeyAccess는 무시됩니다.

이러한 함수가 주어지면 전체 키 세트를 직렬화하는 함수를 작성할 수 있습니다.

String serializeKeyset(KeysetHandle keyset, @Nullable SecretKeyAccess
secretKeyAccess);

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

키 일부에 대한 액세스

Tink 키에는 원시 키 자료뿐만 아니라 키 사용 방법 (그리고 다른 방식으로 사용해서는 안 됨)을 지정하는 메타데이터도 포함됩니다. 예를 들어 Tink의 RSA SSA PSS 키는 이 RSA 키가 지정된 해시 함수와 지정된 솔트 길이를 사용하는 PSS 서명 알고리즘과 함께만 사용될 수 있음을 지정합니다.

Tink 키를 이 메타데이터 중 일부를 명시적으로 지정하지 않는 다른 형식으로 변환해야 하는 경우도 있습니다. 일반적으로 키를 사용할 때 메타데이터를 제공해야 함을 의미합니다. 즉, 키가 항상 동일한 알고리즘으로 사용된다고 가정하면 이러한 키는 암시적으로 동일한 메타데이터를 가지고 있으므로 다른 위치에 저장됩니다.

Tink 키를 다른 형식으로 변환할 때는 Tink 키의 메타데이터가 다른 키 형식의 (암시적으로 지정된) 메타데이터와 일치하는지 확인해야 합니다. 일치하지 않으면 변환에 실패해야 합니다.

이러한 검사가 누락되거나 불완전한 경우가 많으므로 Tink는 부분적이지만 전체 키로 오해될 수 있는 키 자료에 액세스할 수 있는 API에 대한 액세스를 제한합니다. 이를 위해 Java에서는 Tink가 RestrictedApi를 사용하고, C++ 및 Golang에서는 비밀 키 액세스 토큰과 유사한 토큰을 사용합니다.

이러한 API의 사용자는 키 재사용 공격과 비호환성을 모두 방지할 책임이 있습니다.

Tink에서 키를 내보내거나 Tink로 키를 가져올 때 '부분 키 액세스'를 제한하는 방법이 가장 많이 있습니다. 다음 권장사항에서는 이러한 시나리오에서 안전하게 작업하는 방법을 설명합니다.

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

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

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

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

이는 문제가 됩니다. 키를 수신한 후 이를 사용하는 서드 파티는 키의 매개변수를 가정합니다. 예를 들어 이 키 256비트에 사용된 HPKE AEAD 알고리즘이 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라는 유사한 함수가 있는 경우 호출자는 동일한 곡선 점을 사용하여 메시지를 암호화할 수 있으며 이로 인해 쉽게 취약점이 발생할 수 있습니다.

대신 첫 번째 인수가 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();
}

Google에서는 이러한 코드를 사용하여 바이트 배열을 읽는 즉시 Tink 객체로 변환하고 사용할 알고리즘을 완전히 지정합니다. 이 접근 방식은 주요 혼동 공격의 가능성을 최소화합니다.