Skip to content

GH-51669: [C++][Parquet] Preserve cipher in plaintext footer metadata - #54884

Open
autoCAT6 wants to merge 2 commits into
apache:mainfrom
autoCAT6:fix/51669-plaintext-footer-ctr
Open

autoCAT6 wants to merge 2 commits into
apache:mainfrom
autoCAT6:fix/51669-plaintext-footer-ctr

Conversation

@autoCAT6

@autoCAT6 autoCAT6 commented Oct 21, 2026 •

Copy link
Copy Markdown

Rationale for this change

Closes #51669. CTR files with plaintext footers incorrectly declare GCM, causing encrypted-column reads to fail.

What changes are included in this PR?

  • Serialize the configured file encryption algorithm directly.
  • Select AES-GCM explicitly for plaintext footer signing and verification.
  • Add generated GCM/CTR, footer and AAD cases, including an on-disk GCM tag check independent of reader selection.

Original report by dotnwat in #51669. The follow-up incorporates pitrou's review and adamreeve's clarification.

Are these changes tested?

  • Encryption: 45 passed; metadata: 50 passed; writer: 296 passed, with 2 existing disabled tests.
  • Released parquet-java 1.18.1 interoperability: 64 read outcomes and 8 raw metadata inspections matched their controls.
  • clang-format 18 and git diff --check pass.

Are there any user-facing changes?

New CTR/plaintext-footer files declare the correct algorithm. No public API changes.

This PR contains a "Critical Fix": it corrects invalid encryption algorithm metadata emitted by the writer.

Was AI used for this PR?

AI was used to generate the implementation, regression tests, and PR description.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

@github-actions github-actions Bot added the awaiting review Awaiting review label Oct 21, 2026
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51669 has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51669 has no components, please add labels for components.

@pitrou

pitrou commented Oct 21, 2026

Copy link
Copy Markdown
Member

@autoCAT6 This is AI-generated, right?

Have you read our PR template at least?

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51669 has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51669 has no components, please add labels for components.

@autoCAT6

Copy link
Copy Markdown
Author

Thanks for pointing this out, @pitrou. Yes, AI was used for the implementation, regression tests, and PR description. I have updated the description to include the AI disclosure and completed the template checklist.

@pitrou

pitrou commented Oct 22, 2026

Copy link
Copy Markdown
Member

Ok, can you please make the PR description terser so that it only keeps the salient points?

@pitrou

pitrou commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

I'm not an expert on this and the Parquet encryption code is convoluted, but I think this PR is wrong (and so was the previous attempt in #51693).

Basically:

  • the encryption_algorithm recorded in the plaintext footer is the algorithm used for encrypting file metadata and data (it can be either GCM or CTR)
  • the signing algorithm for the plaintext footer is always GCM regardless of the encryption_algorithm

So a proper fix looks IMHO like this:

diff --git a/cpp/src/parquet/encryption/internal_file_encryptor.cc b/cpp/src/parquet/encryption/internal_file_encryptor.cc
index 23dbfe34d3..4ab221fd55 100644
--- a/cpp/src/parquet/encryption/internal_file_encryptor.cc
+++ b/cpp/src/parquet/encryption/internal_file_encryptor.cc
@@ -68,7 +68,9 @@ std::shared_ptr<Encryptor> InternalFileEncryptor::GetFooterSigningEncryptor() {
     return footer_signing_encryptor_;
   }
 
-  ParquetCipher::type algorithm = properties_->algorithm().algorithm;
+  // A plaintext footer is always signed with AES-GCM, regardless of the algorithm
+  // configured for encryption.
+  const auto algorithm = ParquetCipher::AES_GCM_V1;
   std::string footer_aad = encryption::CreateFooterAad(properties_->file_aad());
   const SecureString& footer_signing_key = properties_->footer_key();
   auto aes_encryptor = GetMetaAesEncryptor(algorithm, footer_signing_key.size());
diff --git a/cpp/src/parquet/metadata.cc b/cpp/src/parquet/metadata.cc
index 61a111fc0c..fa0c80442a 100644
--- a/cpp/src/parquet/metadata.cc
+++ b/cpp/src/parquet/metadata.cc
@@ -1202,8 +1202,10 @@ bool FileMetaData::VerifySignature(std::span<const uint8_t> serialized_metadata,
   const SecureString& key = file_decryptor->GetFooterKey();
   const std::string& aad = encryption::CreateFooterAad(file_decryptor->file_aad());
 
+  // A plaintext footer is always signed with AES-GCM, regardless of the algorithm
+  // configured for encryption.
   auto aes_encryptor = encryption::AesEncryptor::Make(
-      file_decryptor->algorithm(), static_cast<int>(key.size()), /*metadata=*/true,
+      ParquetCipher::AES_GCM_V1, static_cast<int>(key.size()), /*metadata=*/true,
       /*write_length=*/false);
 
   std::shared_ptr<Buffer> encrypted_buffer =
@@ -2136,16 +2138,8 @@ class FileMetaDataBuilder::FileMetaDataBuilderImpl {
     // if plaintext footer, set footer signing algorithm
     auto file_encryption_properties = properties_->file_encryption_properties();
     if (file_encryption_properties && !file_encryption_properties->encrypted_footer()) {
-      EncryptionAlgorithm signing_algorithm;
-      EncryptionAlgorithm algo = file_encryption_properties->algorithm();
-      signing_algorithm.aad.aad_file_unique = algo.aad.aad_file_unique;
-      signing_algorithm.aad.supply_aad_prefix = algo.aad.supply_aad_prefix;
-      if (!algo.aad.supply_aad_prefix) {
-        signing_algorithm.aad.aad_prefix = algo.aad.aad_prefix;
-      }
-      signing_algorithm.algorithm = ParquetCipher::AES_GCM_V1;
-
-      metadata_->__set_encryption_algorithm(ToThrift(signing_algorithm));
+      metadata_->__set_encryption_algorithm(
+          ToThrift(file_encryption_properties->algorithm()));
       const std::string& footer_signing_key_metadata =
           file_encryption_properties->footer_key_metadata();
       if (footer_signing_key_metadata.size() > 0) {

cc @adamreeve @EnricoMi who are more knowledgeable than I am.

@pitrou

pitrou commented Oct 22, 2026

Copy link
Copy Markdown
Member

I opened apache/parquet-testing#129 to add a file under parquet-testing.

@adamreeve

Copy link
Copy Markdown
Contributor

So a proper fix looks IMHO like this:

Yes, agreed. It looks like this PR is a partial fix, it correctly sets the file metadata's encryption_algorithm, fixing the issue reported in #51669 and allowing round-tripping within this library. But the files are still incompatible with the spec due to using the file algorithm for footer signing rather than always using AES-GCM.

I also agree with dropping any modifications to signing_algorithm.aad, this should be set correctly already here:

algorithm_.aad.aad_file_unique = aad_file_unique_str;
algorithm_.aad.supply_aad_prefix = supply_aad_prefix;
if (!aad_prefix_.empty() && store_aad_prefix_in_file_) {
algorithm_.aad.aad_prefix = aad_prefix_;
}

@adamreeve

Copy link
Copy Markdown
Contributor

I was also a bit confused about how signing and verifying the footer with AES-CTR would even work, because AES-CTR doesn't write a tag. It looks like we just assume the last 16 bytes of the encrypted data are the GCM tag when writing:

dst->Write(encrypted_data.data() + encrypted_len - encryption::kGcmTagLength,
encryption::kGcmTagLength));

And then do the same when reading:

return 0 == memcmp(encrypted_buffer->data() + encrypted_len - encryption::kGcmTagLength,
tag.data(), encryption::kGcmTagLength);

So the "tag" is actually just the last bytes of the AES-CTR encrypted data and the round trip succeeds

Simplify file algorithm serialization and verify generated plaintext footer tags directly against AES-GCM. Apply the footer signing and metadata simplification suggested in PR review.
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51669 has been automatically assigned in GitHub to PR creator.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51669 has no components, please add labels for components.

@autoCAT6

Copy link
Copy Markdown
Author

Thanks, updated. I shortened the description and simplified the algorithm copy. Signing and both verification paths now explicitly use GCM.

I checked the old code too: metadata=true already selects GCM, and SignedFooterEncrypt rejects CTR. The new footer tag test also passes on the previous head. So the behavior fix is the metadata value; the GCM calls just make the signing rule clear.

C++ tests and the Java 1.18.1 checks pass.

@adamreeve

Copy link
Copy Markdown
Contributor

I checked the old code too: metadata=true already selects GCM, and SignedFooterEncrypt rejects CTR. The new footer tag test also passes on the previous head. So the behavior fix is the metadata value; the GCM calls just make the signing rule clear.

It took me a while to parse this AI speak, but I think this is saying that changing file_decryptor->algorithm() to ParquetCipher::AES_GCM_V1 isn't actually necessary. We end up doing AES-GCM by passing metadata=True, which I missed:

if (metadata || (ParquetCipher::AES_GCM_V1 == alg_id)) {

Passing the AES_GCM_V1 algorithm explicitly makes things clearer. But this is more evidence for a tidy up being necessary (#45525).

I'm happy to create a new parquet-testing file for this. I think we should generate it from this PR branch, then verify it can be read in arrow-go and parquet-java (which are the only other implementations that support AES-CTR I believe).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Plaintext-footer files written with AES_GCM_CTR_V1 record AES_GCM_V1 as the encryption algorithm and cannot be read

3 participants