pdf_oxide 0.3.59

The fastest Rust PDF library with text extraction: 0.8ms mean, 100% pass rate on 3,830 PDFs. 5× faster than pdf_extract, 17× faster than oxidize_pdf. Extract, create, and edit PDFs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
//! FIPS 140-3 validated [`CryptoProvider`] backed by `aws-lc-rs`.
//!
//! Built behind the `fips` cargo feature (off by default).
//! When enabled and installed at runtime via [`super::set_provider`],
//! every PDF crypto operation routes to AWS-LC's FIPS-validated
//! module. Algorithms outside the NIST 140-3 approved set
//! (MD5, RC4, SHA-1 sign, RSA-PKCS#1-v1.5+SHA-1) return
//! [`super::Error::AlgorithmNotPermitted`] — opening a PDF Standard
//! Security R≤4 document, for example, fails with a clear
//! remediation message at [`crate::encryption::handler::EncryptionHandler::new`].
//!
//! # Algorithm coverage
//!
//! | Operation | FIPS-approved? | This provider |
//! |---|---|---|
//! | SHA-256 / 384 / 512 | ✅ | implemented |
//! | MD5 | ❌ | rejected |
//! | SHA-1 hash | (verify-only) | implemented (callers gate sign vs verify) |
//! | AES-128 / 256 CBC PKCS#7 | ✅ | implemented |
//! | AES-128 / 256 CBC no-padding | ✅ | implemented (`unstable::cipher`) |
//! | RC4 | ❌ | rejected |
//! | RSA-PSS verify (SHA-256/384/512) | ✅ | implemented |
//! | RSA-PSS sign | ✅ | not implemented yet (no in-tree caller) |
//! | RSA-PKCS#1-v1.5 verify (SHA-256+) | ✅ | implemented |
//! | RSA-PKCS#1-v1.5 verify (SHA-1) | (verify-only) | implemented |
//! | RSA-PKCS#1-v1.5 sign | ✅ when paired with SHA-256+ | not yet wired (Phase 4 stays on rust-crypto for sign) |
//! | ECDSA P-256 / P-384 verify | ✅ | implemented |
//! | OS RNG | ✅ | `aws_lc_rs::rand::SystemRandom` |
//!
//! Issue #236.

#![cfg(feature = "fips")]

use aws_lc_rs::cipher::{
    DecryptingKey, EncryptingKey, EncryptionContext, PaddedBlockDecryptingKey,
    PaddedBlockEncryptingKey, UnboundCipherKey, AES_128, AES_256,
};
use aws_lc_rs::digest::{Context, SHA1_FOR_LEGACY_USE_ONLY, SHA256, SHA384, SHA512};
use aws_lc_rs::iv::FixedLength;
use aws_lc_rs::rand::{SecureRandom, SystemRandom};
use aws_lc_rs::signature::{
    self, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1,
    RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY, RSA_PKCS1_2048_8192_SHA256,
    RSA_PKCS1_2048_8192_SHA384, RSA_PKCS1_2048_8192_SHA512, RSA_PSS_2048_8192_SHA256,
    RSA_PSS_2048_8192_SHA384, RSA_PSS_2048_8192_SHA512,
};

use super::error::{not_permitted, AlgorithmKind, Error, Result};
use super::provider::{
    CryptoProvider, Hasher, SignatureVerifier, Signer, SigningKeyMaterial, SymmetricCipher,
};
use super::types::{AesKeySize, EcCurve, HashAlgorithm, Padding, RsaPublicKey};

/// FIPS-validated provider backed by `aws-lc-rs`.
#[derive(Debug, Default, Clone, Copy)]
pub struct AwsLcProvider;

impl AwsLcProvider {
    /// Create a new FIPS-validated provider. The AWS-LC FIPS module
    /// runs its required power-on self-test on first crypto call;
    /// failures surface as [`Error::Backend`].
    pub const fn new() -> Self {
        Self
    }
}

impl CryptoProvider for AwsLcProvider {
    fn name(&self) -> &'static str {
        "aws-lc-rs"
    }

    fn is_legacy_allowed(&self) -> bool {
        false
    }

    fn hasher(&self, algo: HashAlgorithm) -> Result<Box<dyn Hasher>> {
        let alg = match algo {
            HashAlgorithm::Md5 => {
                return Err(not_permitted(
                    AlgorithmKind::Hash,
                    "MD5",
                    "FIPS 140-3 forbids MD5 for any use (NIST SP 800-131A)",
                ));
            },
            HashAlgorithm::Sha1 => &SHA1_FOR_LEGACY_USE_ONLY,
            HashAlgorithm::Sha256 => &SHA256,
            HashAlgorithm::Sha384 => &SHA384,
            HashAlgorithm::Sha512 => &SHA512,
        };
        Ok(Box::new(AwsLcHasher {
            ctx: Context::new(alg),
            algo,
        }))
    }

    fn symmetric(&self) -> &dyn SymmetricCipher {
        &AwsLcSymmetric
    }

    fn verifier(&self) -> &dyn SignatureVerifier {
        &AwsLcVerifier
    }

    fn random_bytes(&self, out: &mut [u8]) -> Result<()> {
        SystemRandom::new()
            .fill(out)
            .map_err(|_| Error::Backend("aws_lc_rs SystemRandom failed"))
    }

    fn signer(&self, _key: &SigningKeyMaterial<'_>) -> Result<Box<dyn Signer>> {
        // Signing is not yet wired through this provider — current
        // in-tree caller (`signatures/signer.rs`) builds CMS via
        // `RustCryptoProvider`'s software RSA sign. A follow-up
        // commit will route signing here once the FIPS sign API
        // (`aws_lc_rs::rsa::KeyPair::sign`) integration is tested
        // against the existing CMS test fixtures.
        Err(Error::Backend(
            "AwsLcProvider signing not yet implemented — \
             use RustCryptoProvider for sign or pre-sign with an HSM",
        ))
    }
}

// ---------------------------------------------------------------------------
// Hasher
// ---------------------------------------------------------------------------

struct AwsLcHasher {
    ctx: Context,
    algo: HashAlgorithm,
}

impl Hasher for AwsLcHasher {
    fn update(&mut self, data: &[u8]) {
        self.ctx.update(data);
    }
    fn finalize(self: Box<Self>) -> Vec<u8> {
        self.ctx.finish().as_ref().to_vec()
    }
    fn algorithm(&self) -> HashAlgorithm {
        self.algo
    }
}

// ---------------------------------------------------------------------------
// Symmetric — AES-CBC; RC4 rejected.
// ---------------------------------------------------------------------------

fn aes_alg(key_size: AesKeySize) -> &'static aws_lc_rs::cipher::Algorithm {
    match key_size {
        AesKeySize::Aes128 => &AES_128,
        AesKeySize::Aes256 => &AES_256,
    }
}

fn check_key_iv(key_size: AesKeySize, key: &[u8], iv: &[u8]) -> Result<()> {
    if key.len() != key_size.key_bytes() {
        return Err(Error::InvalidInput(match key_size {
            AesKeySize::Aes128 => "AES-128 requires a 16-byte key",
            AesKeySize::Aes256 => "AES-256 requires a 32-byte key",
        }));
    }
    if iv.len() != 16 {
        return Err(Error::InvalidInput("AES-CBC requires a 16-byte IV"));
    }
    Ok(())
}

struct AwsLcSymmetric;

impl SymmetricCipher for AwsLcSymmetric {
    fn aes_cbc_encrypt(
        &self,
        key_size: AesKeySize,
        key: &[u8],
        iv: &[u8],
        data: &[u8],
        padding: Padding,
    ) -> Result<Vec<u8>> {
        check_key_iv(key_size, key, iv)?;
        let iv_array: [u8; 16] = iv.try_into().expect("checked above");
        let ec = EncryptionContext::Iv128(FixedLength::from(iv_array));
        let alg = aes_alg(key_size);

        match padding {
            Padding::Pkcs7 => {
                let unbound = UnboundCipherKey::new(alg, key)
                    .map_err(|_| Error::Backend("aws_lc_rs UnboundCipherKey::new failed"))?;
                let ek = PaddedBlockEncryptingKey::cbc_pkcs7(unbound).map_err(|_| {
                    Error::Backend("aws_lc_rs PaddedBlockEncryptingKey::cbc_pkcs7 failed")
                })?;
                // PKCS#7 expands input by up to one full block.
                let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 16);
                buf.extend_from_slice(data);
                ek.less_safe_encrypt(&mut buf, ec)
                    .map_err(|_| Error::Backend("aws_lc_rs AES-CBC PKCS#7 encrypt failed"))?;
                Ok(buf)
            },
            Padding::None => {
                if !data.len().is_multiple_of(16) {
                    return Err(Error::InvalidInput(
                        "no-padding AES-CBC requires data length to be a 16-byte multiple",
                    ));
                }
                let unbound = UnboundCipherKey::new(alg, key)
                    .map_err(|_| Error::Backend("aws_lc_rs UnboundCipherKey::new failed"))?;
                let ek = EncryptingKey::cbc(unbound)
                    .map_err(|_| Error::Backend("aws_lc_rs EncryptingKey::cbc failed"))?;
                let mut buf = data.to_vec();
                ek.less_safe_encrypt(&mut buf, ec)
                    .map_err(|_| Error::Backend("aws_lc_rs AES-CBC encrypt failed"))?;
                Ok(buf)
            },
        }
    }

    fn aes_cbc_decrypt(
        &self,
        key_size: AesKeySize,
        key: &[u8],
        iv: &[u8],
        data: &[u8],
        padding: Padding,
    ) -> Result<Vec<u8>> {
        check_key_iv(key_size, key, iv)?;
        if !data.len().is_multiple_of(16) {
            return Err(Error::InvalidInput("AES-CBC ciphertext must be a 16-byte multiple"));
        }
        let iv_array: [u8; 16] = iv.try_into().expect("checked above");
        let dc = aws_lc_rs::cipher::DecryptionContext::Iv128(FixedLength::from(iv_array));
        let alg = aes_alg(key_size);

        match padding {
            Padding::Pkcs7 => {
                let unbound = UnboundCipherKey::new(alg, key)
                    .map_err(|_| Error::Backend("aws_lc_rs UnboundCipherKey::new failed"))?;
                let dk = PaddedBlockDecryptingKey::cbc_pkcs7(unbound).map_err(|_| {
                    Error::Backend("aws_lc_rs PaddedBlockDecryptingKey::cbc_pkcs7 failed")
                })?;
                let mut buf = data.to_vec();
                let pt = dk
                    .decrypt(&mut buf, dc)
                    .map_err(|_| Error::Backend("aws_lc_rs AES-CBC PKCS#7 decrypt failed"))?;
                Ok(pt.to_vec())
            },
            Padding::None => {
                let unbound = UnboundCipherKey::new(alg, key)
                    .map_err(|_| Error::Backend("aws_lc_rs UnboundCipherKey::new failed"))?;
                let dk = DecryptingKey::cbc(unbound)
                    .map_err(|_| Error::Backend("aws_lc_rs DecryptingKey::cbc failed"))?;
                let mut buf = data.to_vec();
                let pt = dk
                    .decrypt(&mut buf, dc)
                    .map_err(|_| Error::Backend("aws_lc_rs AES-CBC decrypt failed"))?;
                Ok(pt.to_vec())
            },
        }
    }

    fn rc4(&self, _key: &[u8], _data: &[u8]) -> Result<Vec<u8>> {
        Err(not_permitted(
            AlgorithmKind::SymmetricCipher,
            "RC4",
            "FIPS 140-3 forbids RC4 — re-encrypt PDF Standard Security R≤4 \
             documents with R=6 (AES-256) under a non-FIPS provider before opening",
        ))
    }
}

// ---------------------------------------------------------------------------
// SignatureVerifier
// ---------------------------------------------------------------------------

struct AwsLcVerifier;

impl SignatureVerifier for AwsLcVerifier {
    fn verify_rsa_pkcs1v15(
        &self,
        pubkey: &RsaPublicKey<'_>,
        hash: HashAlgorithm,
        message: &[u8],
        signature: &[u8],
    ) -> Result<()> {
        // FIPS 140-3: SHA-1 is allowed for *verification* of historical
        // signatures (SP 800-131A) but not for new generation. Use the
        // dedicated legacy algorithm constant; aws-lc-rs still validates
        // via its FIPS module.
        let scheme: &dyn signature::VerificationAlgorithm = match hash {
            HashAlgorithm::Sha1 => &RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY,
            HashAlgorithm::Sha256 => &RSA_PKCS1_2048_8192_SHA256,
            HashAlgorithm::Sha384 => &RSA_PKCS1_2048_8192_SHA384,
            HashAlgorithm::Sha512 => &RSA_PKCS1_2048_8192_SHA512,
            HashAlgorithm::Md5 => {
                return Err(not_permitted(
                    AlgorithmKind::SignatureVerify,
                    "RSA-PKCS#1-v1.5+MD5",
                    "FIPS 140-3 disallows MD5 for signature verification",
                ));
            },
        };
        // aws-lc-rs takes the RSA public key as DER-encoded SubjectPublicKeyInfo.
        let spki = encode_rsa_public_key_der(pubkey)?;
        let key = UnparsedPublicKey::new(scheme, &spki);
        key.verify(message, signature)
            .map_err(|_| Error::Verification("RSA-PKCS#1-v1.5 signature did not verify"))
    }

    fn verify_rsa_pss(
        &self,
        pubkey: &RsaPublicKey<'_>,
        hash: HashAlgorithm,
        message: &[u8],
        signature: &[u8],
    ) -> Result<()> {
        let scheme: &dyn signature::VerificationAlgorithm = match hash {
            HashAlgorithm::Sha256 => &RSA_PSS_2048_8192_SHA256,
            HashAlgorithm::Sha384 => &RSA_PSS_2048_8192_SHA384,
            HashAlgorithm::Sha512 => &RSA_PSS_2048_8192_SHA512,
            HashAlgorithm::Sha1 | HashAlgorithm::Md5 => {
                return Err(not_permitted(
                    AlgorithmKind::SignatureVerify,
                    "RSA-PSS-SHA1/MD5",
                    "FIPS 140-3 disallows PSS with SHA-1 or MD5",
                ));
            },
        };
        let spki = encode_rsa_public_key_der(pubkey)?;
        let key = UnparsedPublicKey::new(scheme, &spki);
        key.verify(message, signature)
            .map_err(|_| Error::Verification("RSA-PSS signature did not verify"))
    }

    fn verify_ecdsa(
        &self,
        curve: EcCurve,
        pubkey_sec1: &[u8],
        message: &[u8],
        signature_der: &[u8],
    ) -> Result<()> {
        let scheme: &dyn signature::VerificationAlgorithm = match curve {
            EcCurve::P256 => &ECDSA_P256_SHA256_ASN1,
            EcCurve::P384 => &ECDSA_P384_SHA384_ASN1,
        };
        let key = UnparsedPublicKey::new(scheme, pubkey_sec1);
        key.verify(message, signature_der)
            .map_err(|_| Error::Verification("ECDSA signature did not verify"))
    }
}

/// Encode a raw `(modulus_be, exponent_be)` RSA public key as DER
/// `SubjectPublicKeyInfo` so `aws_lc_rs::signature::UnparsedPublicKey`
/// can consume it. Hand-rolled minimal ASN.1 — `aws-lc-rs` itself
/// doesn't ship a key-encoder, and adding `der`/`spki` here would
/// pull in the full RustCrypto stack (defeating the FIPS isolation
/// the provider is supposed to give).
fn encode_rsa_public_key_der(pubkey: &RsaPublicKey<'_>) -> Result<Vec<u8>> {
    // RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
    let mut rsapubkey = Vec::new();
    der_seq(&mut rsapubkey, |inner| {
        der_unsigned_int(inner, pubkey.modulus_be);
        der_unsigned_int(inner, pubkey.exponent_be);
    });

    // SubjectPublicKeyInfo ::= SEQUENCE {
    //   algorithm AlgorithmIdentifier,
    //   subjectPublicKey BIT STRING }
    // AlgorithmIdentifier for rsaEncryption = SEQUENCE {
    //   algorithm OID 1.2.840.113549.1.1.1,
    //   parameters NULL }
    const RSA_ENCRYPTION_OID_DER: &[u8] = &[
        0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
    ];
    const NULL_DER: &[u8] = &[0x05, 0x00];

    let mut algo_id = Vec::new();
    algo_id.extend_from_slice(RSA_ENCRYPTION_OID_DER);
    algo_id.extend_from_slice(NULL_DER);

    let mut subject_pk_bitstring = Vec::with_capacity(1 + rsapubkey.len());
    subject_pk_bitstring.push(0x00); // unused-bits prefix
    subject_pk_bitstring.extend_from_slice(&rsapubkey);

    let mut spki = Vec::new();
    der_seq(&mut spki, |inner| {
        der_seq(inner, |a| a.extend_from_slice(&algo_id));
        der_tag_value(inner, 0x03, &subject_pk_bitstring); // BIT STRING
    });
    Ok(spki)
}

fn der_seq<F: FnOnce(&mut Vec<u8>)>(out: &mut Vec<u8>, content: F) {
    let mut inner = Vec::new();
    content(&mut inner);
    der_tag_value(out, 0x30, &inner);
}

fn der_unsigned_int(out: &mut Vec<u8>, be_bytes: &[u8]) {
    // Strip leading zeros, then prepend a 0x00 if MSB is set so DER
    // sees an unsigned integer.
    let mut view = be_bytes;
    while view.len() > 1 && view[0] == 0 {
        view = &view[1..];
    }
    let needs_leading_zero = !view.is_empty() && view[0] & 0x80 != 0;
    let total_len = view.len() + if needs_leading_zero { 1 } else { 0 };
    out.push(0x02);
    der_length(out, total_len);
    if needs_leading_zero {
        out.push(0x00);
    }
    out.extend_from_slice(view);
}

fn der_tag_value(out: &mut Vec<u8>, tag: u8, value: &[u8]) {
    out.push(tag);
    der_length(out, value.len());
    out.extend_from_slice(value);
}

fn der_length(out: &mut Vec<u8>, len: usize) {
    if len < 0x80 {
        out.push(len as u8);
    } else {
        // Long-form length. Up to 4 bytes covers all PDF cert sizes.
        let mut bytes = Vec::with_capacity(4);
        let mut n = len;
        while n > 0 {
            bytes.insert(0, (n & 0xff) as u8);
            n >>= 8;
        }
        out.push(0x80 | bytes.len() as u8);
        out.extend_from_slice(&bytes);
    }
}

// ---------------------------------------------------------------------------
// Tests — focused on policy + the implemented algorithm subset.
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use hex_literal::hex;

    fn provider() -> AwsLcProvider {
        AwsLcProvider::new()
    }

    #[test]
    fn name_and_legacy_policy() {
        let p = provider();
        assert_eq!(p.name(), "aws-lc-rs");
        assert!(!p.is_legacy_allowed());
    }

    #[test]
    fn md5_rejected() {
        let p = provider();
        let result = p.hasher(HashAlgorithm::Md5);
        assert!(matches!(result, Err(Error::AlgorithmNotPermitted { .. })));
    }

    #[test]
    fn rc4_rejected() {
        let p = provider();
        let result = p.symmetric().rc4(b"key", b"data");
        assert!(matches!(result, Err(Error::AlgorithmNotPermitted { .. })));
    }

    #[test]
    fn pss_sha1_rejected() {
        let p = provider();
        let pubkey = RsaPublicKey {
            modulus_be: &[0u8; 256],
            exponent_be: &[0x01, 0x00, 0x01],
        };
        let result =
            p.verifier()
                .verify_rsa_pss(&pubkey, HashAlgorithm::Sha1, b"message", &[0u8; 256]);
        assert!(matches!(result, Err(Error::AlgorithmNotPermitted { .. })));
    }

    // --- Hash vectors against NIST FIPS 180-4 ---

    #[test]
    fn sha256_abc() {
        let p = provider();
        let mut h = p.hasher(HashAlgorithm::Sha256).unwrap();
        h.update(b"abc");
        assert_eq!(
            h.finalize(),
            hex!("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
        );
    }

    #[test]
    fn sha384_abc() {
        let p = provider();
        let mut h = p.hasher(HashAlgorithm::Sha384).unwrap();
        h.update(b"abc");
        assert_eq!(
            h.finalize(),
            hex!(
                "cb00753f45a35e8bb5a03d699ac65007"
                "272c32ab0eded1631a8b605a43ff5bed"
                "8086072ba1e7cc2358baeca134c825a7"
            )
        );
    }

    #[test]
    fn sha512_abc() {
        let p = provider();
        let mut h = p.hasher(HashAlgorithm::Sha512).unwrap();
        h.update(b"abc");
        assert_eq!(
            h.finalize(),
            hex!(
                "ddaf35a193617abacc417349ae204131"
                "12e6fa4e89a97ea20a9eeee64b55d39a"
                "2192992a274fc1a836ba3c23a3feebbd"
                "454d4423643ce80e2a9ac94fa54ca49f"
            )
        );
    }

    // --- AES round-trip ---

    #[test]
    fn aes128_cbc_pkcs7_round_trip() {
        let p = provider();
        let key = [0x42u8; 16];
        let iv = [0x13u8; 16];
        let plaintext = b"PDF Standard Security V=4 stream content.";
        let ct = p
            .symmetric()
            .aes_cbc_encrypt(AesKeySize::Aes128, &key, &iv, plaintext, Padding::Pkcs7)
            .unwrap();
        let pt = p
            .symmetric()
            .aes_cbc_decrypt(AesKeySize::Aes128, &key, &iv, &ct, Padding::Pkcs7)
            .unwrap();
        assert_eq!(pt, plaintext);
    }

    #[test]
    fn aes256_cbc_no_pad_round_trip() {
        let p = provider();
        let key = [0x07u8; 32];
        let iv = [0u8; 16];
        let plaintext = [0xa5u8; 32];
        let ct = p
            .symmetric()
            .aes_cbc_encrypt(AesKeySize::Aes256, &key, &iv, &plaintext, Padding::None)
            .unwrap();
        let pt = p
            .symmetric()
            .aes_cbc_decrypt(AesKeySize::Aes256, &key, &iv, &ct, Padding::None)
            .unwrap();
        assert_eq!(pt, plaintext);
    }

    #[test]
    fn random_bytes_non_constant() {
        let p = provider();
        let mut a = [0u8; 32];
        let mut b = [0u8; 32];
        p.random_bytes(&mut a).unwrap();
        p.random_bytes(&mut b).unwrap();
        assert_ne!(a, b);
    }

    #[test]
    fn cross_provider_aes_compat() {
        // AES-128 is deterministic given key+IV+plaintext+padding —
        // so the FIPS provider must produce byte-identical ciphertext
        // to the default RustCryptoProvider. This catches API
        // mismatches (wrong padding mode, wrong IV interpretation)
        // immediately.
        use super::super::RustCryptoProvider;

        let key = [0x42u8; 16];
        let iv = [0x13u8; 16];
        let plaintext = b"FIPS-vs-rust-crypto byte-equality check.";

        let aws_lc = AwsLcProvider::new();
        let rust = RustCryptoProvider::new();

        let aws_ct = aws_lc
            .symmetric()
            .aes_cbc_encrypt(AesKeySize::Aes128, &key, &iv, plaintext, Padding::Pkcs7)
            .unwrap();
        let rust_ct = rust
            .symmetric()
            .aes_cbc_encrypt(AesKeySize::Aes128, &key, &iv, plaintext, Padding::Pkcs7)
            .unwrap();

        assert_eq!(aws_ct, rust_ct, "FIPS and rust-crypto AES-CBC must match");
    }
}