Skip to content

Commit fb72b8f

Browse files
rokadamreeve
andauthored
Verify footer tags when reading encrypted Parquet files with plaintext footers (#7459)
* Initial commit * Lint and clippy * Plaintext layout is different to encrypted one * Lint and expected memory size at decryption * Apply suggestions from code review Co-authored-by: Adam Reeve <[email protected]> * Review feedback * Lint * Update parquet/tests/encryption/encryption.rs Co-authored-by: Adam Reeve <[email protected]> * Review feedback --------- Co-authored-by: Adam Reeve <[email protected]>
1 parent 4d1d79c commit fb72b8f

File tree

5 files changed

+102
-11
lines changed

5 files changed

+102
-11
lines changed

parquet/src/encryption/ciphers.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ use ring::rand::{SecureRandom, SystemRandom};
2323
use std::fmt::Debug;
2424

2525
const RIGHT_TWELVE: u128 = 0x0000_0000_ffff_ffff_ffff_ffff_ffff_ffff;
26-
const NONCE_LEN: usize = 12;
27-
const TAG_LEN: usize = 16;
28-
const SIZE_LEN: usize = 4;
26+
pub(crate) const NONCE_LEN: usize = 12;
27+
pub(crate) const TAG_LEN: usize = 16;
28+
pub(crate) const SIZE_LEN: usize = 4;
2929

3030
pub(crate) trait BlockDecryptor: Debug + Send + Sync {
3131
fn decrypt(&self, length_and_ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>>;
32+
33+
fn compute_plaintext_tag(&self, aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>>;
3234
}
3335

3436
#[derive(Debug, Clone)]
@@ -63,6 +65,19 @@ impl BlockDecryptor for RingGcmBlockDecryptor {
6365
result.resize(result.len() - TAG_LEN, 0u8);
6466
Ok(result)
6567
}
68+
69+
fn compute_plaintext_tag(&self, aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
70+
let mut plaintext = plaintext.to_vec();
71+
let nonce = &plaintext[plaintext.len() - NONCE_LEN - TAG_LEN..plaintext.len() - TAG_LEN];
72+
let nonce = ring::aead::Nonce::try_assume_unique_for_key(nonce)?;
73+
let plaintext_end = plaintext.len() - NONCE_LEN - TAG_LEN;
74+
let tag = self.key.seal_in_place_separate_tag(
75+
nonce,
76+
Aad::from(aad),
77+
&mut plaintext[..plaintext_end],
78+
)?;
79+
Ok(tag.as_ref().to_vec())
80+
}
6681
}
6782

6883
pub(crate) trait BlockEncryptor: Debug + Send + Sync {

parquet/src/encryption/decrypt.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
//! Configuration and utilities for decryption of files using Parquet Modular Encryption
1919
20-
use crate::encryption::ciphers::{BlockDecryptor, RingGcmBlockDecryptor};
21-
use crate::encryption::modules::{create_module_aad, ModuleType};
20+
use crate::encryption::ciphers::{BlockDecryptor, RingGcmBlockDecryptor, TAG_LEN};
21+
use crate::encryption::modules::{create_footer_aad, create_module_aad, ModuleType};
2222
use crate::errors::{ParquetError, Result};
2323
use crate::file::column_crypto_metadata::ColumnCryptoMetaData;
2424
use std::borrow::Cow;
@@ -331,6 +331,7 @@ impl PartialEq for DecryptionKeys {
331331
pub struct FileDecryptionProperties {
332332
keys: DecryptionKeys,
333333
aad_prefix: Option<Vec<u8>>,
334+
footer_signature_verification: bool,
334335
}
335336

336337
impl FileDecryptionProperties {
@@ -351,6 +352,11 @@ impl FileDecryptionProperties {
351352
self.aad_prefix.as_ref()
352353
}
353354

355+
/// Returns true if footer signature verification is enabled for files with plaintext footers.
356+
pub fn check_plaintext_footer_integrity(&self) -> bool {
357+
self.footer_signature_verification
358+
}
359+
354360
/// Get the encryption key for decrypting a file's footer,
355361
/// and also column data if uniform encryption is used.
356362
pub fn footer_key(&self, key_metadata: Option<&[u8]>) -> Result<Cow<Vec<u8>>> {
@@ -415,6 +421,7 @@ pub struct DecryptionPropertiesBuilder {
415421
key_retriever: Option<Arc<dyn KeyRetriever>>,
416422
column_keys: HashMap<String, Vec<u8>>,
417423
aad_prefix: Option<Vec<u8>>,
424+
footer_signature_verification: bool,
418425
}
419426

420427
impl DecryptionPropertiesBuilder {
@@ -426,6 +433,7 @@ impl DecryptionPropertiesBuilder {
426433
key_retriever: None,
427434
column_keys: HashMap::default(),
428435
aad_prefix: None,
436+
footer_signature_verification: true,
429437
}
430438
}
431439

@@ -439,6 +447,7 @@ impl DecryptionPropertiesBuilder {
439447
key_retriever: Some(key_retriever),
440448
column_keys: HashMap::default(),
441449
aad_prefix: None,
450+
footer_signature_verification: true,
442451
}
443452
}
444453

@@ -464,6 +473,7 @@ impl DecryptionPropertiesBuilder {
464473
Ok(FileDecryptionProperties {
465474
keys,
466475
aad_prefix: self.aad_prefix,
476+
footer_signature_verification: self.footer_signature_verification,
467477
})
468478
}
469479

@@ -496,6 +506,13 @@ impl DecryptionPropertiesBuilder {
496506
}
497507
Ok(self)
498508
}
509+
510+
/// Disable verification of footer tags for files that use plaintext footers.
511+
/// Signature verification is enabled by default.
512+
pub fn disable_footer_signature_verification(mut self) -> Self {
513+
self.footer_signature_verification = false;
514+
self
515+
}
499516
}
500517

501518
#[derive(Clone, Debug)]
@@ -538,6 +555,25 @@ impl FileDecryptor {
538555
Ok(self.footer_decryptor.clone())
539556
}
540557

558+
/// Verify the signature of the footer
559+
pub(crate) fn verify_plaintext_footer_signature(&self, plaintext_footer: &[u8]) -> Result<()> {
560+
// Plaintext footer format is: [plaintext metadata, nonce, authentication tag]
561+
let tag = &plaintext_footer[plaintext_footer.len() - TAG_LEN..];
562+
let aad = create_footer_aad(self.file_aad())?;
563+
let footer_decryptor = self.get_footer_decryptor()?;
564+
565+
let computed_tag = footer_decryptor.compute_plaintext_tag(&aad, plaintext_footer)?;
566+
567+
if computed_tag != tag {
568+
return Err(general_err!(
569+
"Footer signature verification failed. Computed: {:?}, Expected: {:?}",
570+
computed_tag,
571+
tag
572+
));
573+
}
574+
Ok(())
575+
}
576+
541577
pub(crate) fn get_column_data_decryptor(
542578
&self,
543579
column_name: &str,

parquet/src/file/metadata/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,7 +1999,7 @@ mod tests {
19991999
#[cfg(not(feature = "encryption"))]
20002000
let base_expected_size = 2312;
20012001
#[cfg(feature = "encryption")]
2002-
let base_expected_size = 2640;
2002+
let base_expected_size = 2648;
20032003

20042004
assert_eq!(parquet_meta.memory_size(), base_expected_size);
20052005

@@ -2029,7 +2029,7 @@ mod tests {
20292029
#[cfg(not(feature = "encryption"))]
20302030
let bigger_expected_size = 2816;
20312031
#[cfg(feature = "encryption")]
2032-
let bigger_expected_size = 3144;
2032+
let bigger_expected_size = 3152;
20332033

20342034
// more set fields means more memory usage
20352035
assert!(bigger_expected_size > base_expected_size);

parquet/src/file/metadata/reader.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@
1717

1818
use std::{io::Read, ops::Range, sync::Arc};
1919

20-
use bytes::Bytes;
21-
2220
use crate::basic::ColumnOrder;
2321
#[cfg(feature = "encryption")]
2422
use crate::encryption::{
2523
decrypt::{FileDecryptionProperties, FileDecryptor},
2624
modules::create_footer_aad,
2725
};
26+
use bytes::Bytes;
2827

2928
use crate::errors::{ParquetError, Result};
3029
use crate::file::metadata::{ColumnChunkMetaData, FileMetaData, ParquetMetaData, RowGroupMetaData};
@@ -967,11 +966,15 @@ impl ParquetMetaDataReader {
967966
file_decryption_properties,
968967
) {
969968
// File has a plaintext footer but encryption algorithm is set
970-
file_decryptor = Some(get_file_decryptor(
969+
let file_decryptor_value = get_file_decryptor(
971970
algo,
972971
t_file_metadata.footer_signing_key_metadata.as_deref(),
973972
file_decryption_properties,
974-
)?);
973+
)?;
974+
if file_decryption_properties.check_plaintext_footer_integrity() && !encrypted_footer {
975+
file_decryptor_value.verify_plaintext_footer_signature(buf)?;
976+
}
977+
file_decryptor = Some(file_decryptor_value);
975978
}
976979

977980
let mut row_groups = Vec::new();

parquet/tests/encryption/encryption.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,43 @@ fn test_non_uniform_encryption_plaintext_footer() {
6060
verify_encryption_test_file_read(file, decryption_properties);
6161
}
6262

63+
#[test]
64+
fn test_plaintext_footer_signature_verification() {
65+
let test_data = arrow::util::test_util::parquet_test_data();
66+
let path = format!("{test_data}/encrypt_columns_plaintext_footer.parquet.encrypted");
67+
let file = File::open(path.clone()).unwrap();
68+
69+
let footer_key = "0000000000000000".as_bytes(); // 128bit/16
70+
let column_1_key = "1234567890123450".as_bytes();
71+
let column_2_key = "1234567890123451".as_bytes();
72+
73+
let decryption_properties = FileDecryptionProperties::builder(footer_key.to_vec())
74+
.disable_footer_signature_verification()
75+
.with_column_key("double_field", column_1_key.to_vec())
76+
.with_column_key("float_field", column_2_key.to_vec())
77+
.build()
78+
.unwrap();
79+
80+
verify_encryption_test_file_read(file, decryption_properties);
81+
82+
let file = File::open(path.clone()).unwrap();
83+
84+
let decryption_properties = FileDecryptionProperties::builder(footer_key.to_vec())
85+
.with_column_key("double_field", column_1_key.to_vec())
86+
.with_column_key("float_field", column_2_key.to_vec())
87+
.build()
88+
.unwrap();
89+
90+
let options = ArrowReaderOptions::default()
91+
.with_file_decryption_properties(decryption_properties.clone());
92+
let result = ArrowReaderMetadata::load(&file, options.clone());
93+
assert!(result.is_err());
94+
assert!(result
95+
.unwrap_err()
96+
.to_string()
97+
.starts_with("Parquet error: Footer signature verification failed. Computed: ["));
98+
}
99+
63100
#[test]
64101
fn test_non_uniform_encryption_disabled_aad_storage() {
65102
let test_data = arrow::util::test_util::parquet_test_data();

0 commit comments

Comments
 (0)