gmcrypto-core 0.5.1

Constant-time-designed pure-Rust SM2/SM3 primitives (no_std + alloc) with an in-CI dudect timing-leak regression harness
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
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
//! PKCS#8 `OneAsymmetricKey` codec (RFC 5958) + PBES2 encryption (RFC 8018).
//!
//! Wire shapes:
//!
//! ```text
//! OneAsymmetricKey ::= SEQUENCE {
//!     version              INTEGER (0),
//!     privateKeyAlgorithm  AlgorithmIdentifier,
//!     privateKey           OCTET STRING -- DER-encoded ECPrivateKey
//! }
//!
//! EncryptedPrivateKeyInfo ::= SEQUENCE {
//!     encryptionAlgorithm  AlgorithmIdentifier,
//!     encryptedData        OCTET STRING
//! }
//! ```
//!
//! For SM2 the `privateKeyAlgorithm` is `id-ecPublicKey` with
//! `namedCurve = sm2p256v1`; `privateKey` wraps a DER-encoded RFC 5915
//! [`crate::sec1::EcPrivateKey`].
//!
//! For the encrypted variant, `encryptionAlgorithm` is `id-PBES2`
//! with `keyDerivationFunc = id-PBKDF2` (PRF = id-hmacWithSM3) and
//! `encryptionScheme = sm4-cbc` (IV in the parameters OCTET STRING).
//! `encryptedData` is the SM4-CBC ciphertext of the inner
//! `OneAsymmetricKey`.
//!
//! # Failure-mode invariant (API)
//!
//! All decoders return `Result<_, Error>` with a single
//! [`Error::Failed`] variant. The return type carries no distinction
//! between "wrong password", "malformed PEM", and "valid PEM but
//! bad inner `ECPrivateKey`" — the caller sees one uninformative
//! shape on any failure.
//!
//! # Timing-side-channel posture
//!
//! Code paths over **secret material** are constant-time-designed:
//! PBKDF2-HMAC-SM3 (covered by [`crate::hmac`]'s constant-time
//! discipline), SM4-CBC decrypt + PKCS#7 strip
//! ([`crate::sm4::mode_cbc::decrypt`]), and the inner
//! [`Sm2PrivateKey::from_bytes_be`] range gate. The W2 dudect target
//! `ct_pkcs8_decrypt` class-splits by **password bytes** (both
//! classes ship valid blobs so both succeed via identical control
//! flow); local 10K-sample run measures `|tau| ≈ 0.02`.
//!
//! Code paths over **public attacker-supplied wire bytes** (the
//! PBES2 structural parse) early-return on malformed input. A
//! structurally invalid blob fails in microseconds; a structurally
//! valid blob with a wrong password runs full PBKDF2 + SM4-CBC + an
//! inner parse before failing. **This wall-clock distinction is
//! observable but not secret-dependent** — the attacker built the
//! blob and already knows its structural validity. The dudect
//! gate above covers the only secret-dependent timing class
//! (password vs password under a valid blob).
//!
//! # KDF parameter validation
//!
//! [`decrypt`] rejects `iterations == 0` (RFC 8018 §5.2 requires
//! `c ≥ 1`) and `iterations > 10_000_000` (denial-of-service
//! bound; the upper limit is per the W2 risk-trap in
//! `docs/v0.3-scope.md`). Any other malformed PBES2 parameter
//! folds into [`Error::Failed`] identically.

use crate::asn1::oid::{ID_EC_PUBLIC_KEY, ID_HMAC_WITH_SM3, ID_PBKDF2, PBES2, SM2P256V1, SM4_CBC};
use crate::asn1::{reader, writer};
use crate::kdf::pbkdf2_hmac_sm3;
use crate::sec1;
use crate::sm2::Sm2PrivateKey;
use crate::sm4::mode_cbc;
use alloc::vec::Vec;
use crypto_bigint::U256;
use subtle::ConstantTimeEq;
use zeroize::Zeroize;

/// PKCS#8 codec failure — alias for the workspace-wide [`crate::Error`].
///
/// Single uninformative variant per the project's failure-mode
/// invariant. Prior to v0.5 this was a distinct `pkcs8::Error` enum;
/// v0.5 W5 unifies it with the workspace-wide type via this alias,
/// so import paths and non-exhaustive `match` callsites against
/// `pkcs8::Error::Failed` continue to work. **One caveat:** the
/// workspace-wide type is `#[non_exhaustive]`, so downstream
/// **exhaustive** `match` arms must now add a wildcard `_ => ...`
/// (single-variant non-exhaustive enums require the wildcard from
/// outside-crate matches).
pub type Error = crate::Error;

/// PKCS#8 version field (`v1 = 0`). RFC 5958 also defines `v2 = 1`
/// when the optional `publicKey` BIT STRING is present; v0.3 emits
/// `v1` and accepts both `v1` and `v2`.
const PKCS8_V1: u8 = 0;
const PKCS8_V2: u8 = 1;

/// Default SM4-CBC key length (always 16 bytes for SM4).
const SM4_KEY_LEN: usize = 16;
/// SM4 block size = CBC IV size.
const SM4_IV_LEN: usize = 16;

/// PBKDF2 iteration count upper bound for [`decrypt`].
///
/// Rejects adversarial blobs that would burn unbounded CPU. v0.3
/// picks `10_000_000` as the denial-of-service ceiling — well
/// above any realistic production iteration count and still
/// bounded.
pub const PBKDF2_MAX_ITERATIONS: u32 = 10_000_000;

/// Encode an SM2 private key as a DER-encoded **unencrypted**
/// PKCS#8 `OneAsymmetricKey`.
///
/// The inner `ECPrivateKey` carries the scalar plus the optional
/// `publicKey` field (uncompressed `04 || X || Y`); the outer
/// privateKeyAlgorithm is `id-ecPublicKey` with `sm2p256v1`.
///
/// The intermediate `ECPrivateKey` body is zeroized before return.
/// **The returned `Vec<u8>` contains the raw scalar bytes** —
/// caller is responsible for wiping it before letting it leave
/// their stack frame.
#[must_use]
pub fn encode(key: &Sm2PrivateKey) -> Vec<u8> {
    let mut scalar_be = key.to_bytes_be();
    let pub_uncompressed = {
        let pub_key = crate::sm2::Sm2PublicKey::from_point(key.public_key());
        pub_key.to_sec1_uncompressed()
    };
    let mut inner = sec1::encode(&scalar_be, Some(&pub_uncompressed));
    scalar_be.zeroize();

    // privateKeyAlgorithm SEQUENCE { id-ecPublicKey, namedCurve }
    let mut alg_inner = Vec::with_capacity(ID_EC_PUBLIC_KEY.len() + SM2P256V1.len() + 4);
    writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
    writer::write_oid(&mut alg_inner, SM2P256V1);
    let mut alg_seq = Vec::with_capacity(alg_inner.len() + 4);
    writer::write_sequence(&mut alg_seq, &alg_inner);

    let mut body = Vec::with_capacity(inner.len() + alg_seq.len() + 8);
    writer::write_integer(&mut body, &[PKCS8_V1]);
    body.extend_from_slice(&alg_seq);
    writer::write_octet_string(&mut body, &inner);

    let mut out = Vec::with_capacity(body.len() + 4);
    writer::write_sequence(&mut out, &body);

    // Wipe the secret-bearing intermediates. (Zeroize is a side-
    // effecting write the optimizer must preserve via the
    // `volatile_write` in `zeroize::Zeroize`; the buffer is read by
    // SM4-CBC during the encrypt path's previous use.)
    inner.zeroize();
    body.zeroize();
    out
}

/// Decode an unencrypted PKCS#8 `OneAsymmetricKey` blob into an
/// [`Sm2PrivateKey`].
///
/// Validates the version (0 or 1), the privateKeyAlgorithm
/// (id-ecPublicKey + sm2p256v1), the inner `ECPrivateKey` scalar
/// (`d ∈ [1, n-2]`), and any optional public key (must match
/// `d·G`).
///
/// # Errors
///
/// Returns [`Error::Failed`] for any malformed input or
/// out-of-range scalar.
pub fn decode(input: &[u8]) -> Result<Sm2PrivateKey, Error> {
    let (body, rest) = reader::read_sequence(input).ok_or(Error::Failed)?;
    if !rest.is_empty() {
        return Err(Error::Failed);
    }

    // version INTEGER
    let (version, body) = reader::read_integer(body).ok_or(Error::Failed)?;
    if version != [PKCS8_V1] && version != [PKCS8_V2] {
        return Err(Error::Failed);
    }

    // privateKeyAlgorithm SEQUENCE
    let (alg_inner, body) = reader::read_sequence(body).ok_or(Error::Failed)?;
    let (alg_oid, alg_inner) = reader::read_oid(alg_inner).ok_or(Error::Failed)?;
    if alg_oid != ID_EC_PUBLIC_KEY {
        return Err(Error::Failed);
    }
    let (curve_oid, alg_inner) = reader::read_oid(alg_inner).ok_or(Error::Failed)?;
    if curve_oid != SM2P256V1 || !alg_inner.is_empty() {
        return Err(Error::Failed);
    }

    // privateKey OCTET STRING { ECPrivateKey }
    let (inner_bytes, body) = reader::read_octet_string(body).ok_or(Error::Failed)?;
    let mut inner = sec1::decode(inner_bytes).ok_or(Error::Failed)?;

    // Trailing PKCS#8 v2 attributes/publicKey are tolerated but not
    // required; we reject any unrecognized tag to stay strict.
    // (gmssl 3.1.1 emits PKCS#8 v1 by default, which has no trailing
    // fields.)
    if !body.is_empty() {
        // PKCS#8 v2 may carry [0] Attributes and [1] BIT STRING
        // publicKey. Skip them tolerantly: we don't need either,
        // since the inner ECPrivateKey already carries the public
        // point and the scalar is authoritative.
        let mut tail = body;
        while !tail.is_empty() {
            // Try [0] attributes EXPLICIT.
            if let Some((_, after)) = reader::read_context_tagged_explicit(tail, 0) {
                tail = after;
                continue;
            }
            // Try [1] publicKey EXPLICIT BIT STRING.
            if let Some((_, after)) = reader::read_context_tagged_explicit(tail, 1) {
                tail = after;
                continue;
            }
            // Unknown tag — fold into Failed.
            inner.scalar_be.zeroize();
            return Err(Error::Failed);
        }
    }

    let d = U256::from_be_slice(&inner.scalar_be);
    inner.scalar_be.zeroize();
    let key = Sm2PrivateKey::from_scalar_inner(d);
    let key: Option<Sm2PrivateKey> = key.into();
    let key = key.ok_or(Error::Failed)?;

    // If the inner ECPrivateKey carried a publicKey, cross-check it
    // matches d·G — defends against stripped-public-key + stripped-
    // scalar swaps that escape unencrypted PKCS#8 detection.
    if let Some(stored_pub) = inner.public {
        let derived = key.public_key();
        if !bool::from(stored_pub.ct_eq(&derived)) {
            return Err(Error::Failed);
        }
    }

    Ok(key)
}

/// Encrypt an SM2 private key as a DER-encoded RFC 5958
/// `EncryptedPrivateKeyInfo` blob using PBES2 (PBKDF2-HMAC-SM3 +
/// SM4-CBC).
///
/// `salt` and `iv` must be **caller-supplied unpredictable
/// CSPRNG output** — see `CLAUDE.md` and
/// [`crate::sm4::mode_cbc::encrypt`]'s IV contract. Re-using the
/// same `(salt, iv)` under the same password under two different
/// keys is a key-recovery attack.
///
/// `iterations` should be at least 600,000 (OWASP 2024 PBKDF2
/// baseline); v0.3 does not enforce a minimum to keep test
/// vectors reproducible cheaply, but production callers must
/// pick.
///
/// # Errors
///
/// Returns [`Error::Failed`] only if `iterations == 0` (RFC 8018
/// `c ≥ 1`).
pub fn encrypt(
    key: &Sm2PrivateKey,
    password: &[u8],
    salt: &[u8],
    iterations: u32,
    iv: &[u8; SM4_IV_LEN],
) -> Result<Vec<u8>, Error> {
    if iterations == 0 {
        return Err(Error::Failed);
    }

    // 1. Encode the inner OneAsymmetricKey.
    let mut inner = encode(key);

    // 2. Derive an SM4-CBC key: PBKDF2-HMAC-SM3(password, salt, iter, 16).
    let mut sm4_key = [0u8; SM4_KEY_LEN];
    pbkdf2_hmac_sm3(password, salt, iterations, &mut sm4_key).ok_or(Error::Failed)?;

    // 3. Encrypt under SM4-CBC + PKCS#7 padding.
    let ciphertext = mode_cbc::encrypt(&sm4_key, iv, &inner);

    // Wipe sensitive intermediates.
    inner.zeroize();
    sm4_key.zeroize();

    // 4. Build the EncryptedPrivateKeyInfo wrapper.
    let pbes2_params = build_pbes2_params(salt, iterations, iv);
    let mut alg_inner = Vec::with_capacity(PBES2.len() + pbes2_params.len() + 4);
    writer::write_oid(&mut alg_inner, PBES2);
    alg_inner.extend_from_slice(&pbes2_params);
    let mut alg_seq = Vec::with_capacity(alg_inner.len() + 4);
    writer::write_sequence(&mut alg_seq, &alg_inner);

    let mut body = Vec::with_capacity(alg_seq.len() + ciphertext.len() + 8);
    body.extend_from_slice(&alg_seq);
    writer::write_octet_string(&mut body, &ciphertext);

    let mut out = Vec::with_capacity(body.len() + 4);
    writer::write_sequence(&mut out, &body);
    Ok(out)
}

/// Build the PBES2 `parameters` SEQUENCE for [`encrypt`].
///
/// ```text
/// PBES2-params ::= SEQUENCE {
///     keyDerivationFunc  AlgorithmIdentifier { id-PBKDF2,  PBKDF2-params },
///     encryptionScheme   AlgorithmIdentifier { sm4-cbc,    OCTET STRING (IV) }
/// }
///
/// PBKDF2-params ::= SEQUENCE {
///     salt            OCTET STRING,
///     iterationCount  INTEGER,
///     keyLength       INTEGER OPTIONAL,
///     prf             AlgorithmIdentifier { id-hmacWithSM3, NULL }
/// }
/// ```
fn build_pbes2_params(salt: &[u8], iterations: u32, iv: &[u8; SM4_IV_LEN]) -> Vec<u8> {
    // PBKDF2-params.
    let mut pbkdf2_inner = Vec::with_capacity(salt.len() + 32);
    writer::write_octet_string(&mut pbkdf2_inner, salt);
    writer::write_integer(&mut pbkdf2_inner, &iterations.to_be_bytes());
    // PRF AlgorithmIdentifier { id-hmacWithSM3, NULL }
    let mut prf_inner = Vec::with_capacity(ID_HMAC_WITH_SM3.len() + 4);
    writer::write_oid(&mut prf_inner, ID_HMAC_WITH_SM3);
    writer::write_null(&mut prf_inner);
    let mut prf_seq = Vec::with_capacity(prf_inner.len() + 4);
    writer::write_sequence(&mut prf_seq, &prf_inner);
    pbkdf2_inner.extend_from_slice(&prf_seq);

    let mut pbkdf2_seq = Vec::with_capacity(pbkdf2_inner.len() + 4);
    writer::write_sequence(&mut pbkdf2_seq, &pbkdf2_inner);

    // keyDerivationFunc AlgorithmIdentifier { id-PBKDF2, PBKDF2-params }.
    let mut kdf_inner = Vec::with_capacity(ID_PBKDF2.len() + pbkdf2_seq.len() + 4);
    writer::write_oid(&mut kdf_inner, ID_PBKDF2);
    kdf_inner.extend_from_slice(&pbkdf2_seq);
    let mut kdf_seq = Vec::with_capacity(kdf_inner.len() + 4);
    writer::write_sequence(&mut kdf_seq, &kdf_inner);

    // encryptionScheme AlgorithmIdentifier { sm4-cbc, OCTET STRING (IV) }.
    let mut es_inner = Vec::with_capacity(SM4_CBC.len() + iv.len() + 4);
    writer::write_oid(&mut es_inner, SM4_CBC);
    writer::write_octet_string(&mut es_inner, iv);
    let mut es_seq = Vec::with_capacity(es_inner.len() + 4);
    writer::write_sequence(&mut es_seq, &es_inner);

    let mut params_inner = Vec::with_capacity(kdf_seq.len() + es_seq.len());
    params_inner.extend_from_slice(&kdf_seq);
    params_inner.extend_from_slice(&es_seq);

    let mut out = Vec::with_capacity(params_inner.len() + 4);
    writer::write_sequence(&mut out, &params_inner);
    out
}

/// Decrypt a DER-encoded RFC 5958 `EncryptedPrivateKeyInfo` blob.
///
/// Parses the PBES2 parameters, derives the SM4-CBC key from
/// `password`, decrypts, and reconstructs the inner SM2 private
/// key. Single uninformative outcome — no path to distinguish
/// "wrong password" from "malformed PBES2 parameters" from
/// "decrypt succeeded but inner key was malformed".
///
/// # Errors
///
/// Returns [`Error::Failed`] for any malformed input. Same shape
/// regardless of where the failure occurred.
pub fn decrypt(input: &[u8], password: &[u8]) -> Result<Sm2PrivateKey, Error> {
    let parsed = parse_encrypted_blob(input).ok_or(Error::Failed)?;

    // Derive the SM4-CBC key from password + salt + iterations.
    let mut sm4_key = [0u8; SM4_KEY_LEN];
    let derive_ok =
        pbkdf2_hmac_sm3(password, parsed.salt, parsed.iterations, &mut sm4_key).is_some();
    // Even if derive failed (iterations == 0 caught above by the
    // 1..=PBKDF2_MAX_ITERATIONS gate, so this is unreachable in
    // practice), continue through decrypt with a zero key to keep
    // failure-mode shapes uniform.
    let _ = derive_ok;

    // SM4-CBC decrypt + PKCS#7 strip. Folds into Failed on any
    // failure (length not multiple of 16, bad pad, etc.).
    let plaintext = mode_cbc::decrypt(&sm4_key, &parsed.iv, parsed.ciphertext);
    sm4_key.zeroize();

    let mut plaintext = plaintext.ok_or(Error::Failed)?;

    // Parse the inner unencrypted OneAsymmetricKey. Failure folds
    // into the same Failed.
    let result = decode(&plaintext);
    plaintext.zeroize();
    result
}

/// Internal: parsed PBES2 parameters + ciphertext.
struct ParsedEncrypted<'a> {
    salt: &'a [u8],
    iterations: u32,
    iv: [u8; SM4_IV_LEN],
    ciphertext: &'a [u8],
}

/// Parse an `EncryptedPrivateKeyInfo` blob into validated PBES2
/// parameters + the ciphertext slice. Returns `None` for any
/// malformed input or unsupported algorithm.
fn parse_encrypted_blob(input: &[u8]) -> Option<ParsedEncrypted<'_>> {
    let (body, rest) = reader::read_sequence(input)?;
    if !rest.is_empty() {
        return None;
    }
    // encryptionAlgorithm SEQUENCE { id-PBES2, PBES2-params }.
    let (alg_inner, body) = reader::read_sequence(body)?;
    let (alg_oid, alg_inner) = reader::read_oid(alg_inner)?;
    if alg_oid != PBES2 {
        return None;
    }
    let (params_inner, alg_inner_rest) = reader::read_sequence(alg_inner)?;
    if !alg_inner_rest.is_empty() {
        return None;
    }

    // keyDerivationFunc SEQUENCE { id-PBKDF2, PBKDF2-params }.
    let (kdf_seq, params_rest) = reader::read_sequence(params_inner)?;
    let (kdf_oid, kdf_after) = reader::read_oid(kdf_seq)?;
    if kdf_oid != ID_PBKDF2 {
        return None;
    }
    let (pbkdf2_inner, kdf_seq_rest) = reader::read_sequence(kdf_after)?;
    if !kdf_seq_rest.is_empty() {
        return None;
    }

    // PBKDF2-params: salt, iterations, [keyLength], [PRF].
    let (salt, pbkdf2_inner) = reader::read_octet_string(pbkdf2_inner)?;
    let (iter_bytes, mut pbkdf2_inner) = reader::read_integer(pbkdf2_inner)?;
    if iter_bytes.len() > 4 {
        return None;
    }
    let mut iter_buf = [0u8; 4];
    iter_buf[4 - iter_bytes.len()..].copy_from_slice(iter_bytes);
    let iterations = u32::from_be_bytes(iter_buf);
    if iterations == 0 || iterations > PBKDF2_MAX_ITERATIONS {
        return None;
    }
    // Optional keyLength. SM4-CBC fixes 16 bytes; if present, must equal 16.
    if let Some((kl_bytes, after)) = reader::read_integer(pbkdf2_inner) {
        if kl_bytes.len() > 4 {
            return None;
        }
        let mut kl_buf = [0u8; 4];
        kl_buf[4 - kl_bytes.len()..].copy_from_slice(kl_bytes);
        let key_length = u32::from_be_bytes(kl_buf) as usize;
        if key_length != SM4_KEY_LEN {
            return None;
        }
        pbkdf2_inner = after;
    }
    // Optional PRF. Default is HMAC-SHA-1 per RFC 8018; we accept
    // only id-hmacWithSM3 (gmssl convention; required for SM2 PKCS#8).
    // No PRF specified would default to HMAC-SHA1, which is invalid
    // for SM2 PKCS#8 — reject.
    if pbkdf2_inner.is_empty() {
        return None;
    }
    let (prf_seq, prf_rest) = reader::read_sequence(pbkdf2_inner)?;
    if !prf_rest.is_empty() {
        return None;
    }
    let (prf_oid, prf_seq_rest) = reader::read_oid(prf_seq)?;
    if prf_oid != ID_HMAC_WITH_SM3 {
        return None;
    }
    // The PRF parameters MAY be NULL or absent.
    if !prf_seq_rest.is_empty()
        && (reader::read_null(prf_seq_rest).is_none() || prf_seq_rest.len() != 2)
    {
        return None;
    }

    // encryptionScheme SEQUENCE { sm4-cbc, OCTET STRING (IV) }.
    let (es_seq, params_outer_rest) = reader::read_sequence(params_rest)?;
    if !params_outer_rest.is_empty() {
        return None;
    }
    let (es_oid, es_after) = reader::read_oid(es_seq)?;
    if es_oid != SM4_CBC {
        return None;
    }
    let (iv_bytes, es_seq_rest) = reader::read_octet_string(es_after)?;
    if !es_seq_rest.is_empty() || iv_bytes.len() != SM4_IV_LEN {
        return None;
    }
    let mut iv = [0u8; SM4_IV_LEN];
    iv.copy_from_slice(iv_bytes);

    // encryptedData OCTET STRING.
    let (ciphertext, body_rest) = reader::read_octet_string(body)?;
    if !body_rest.is_empty() {
        return None;
    }

    Some(ParsedEncrypted {
        salt,
        iterations,
        iv,
        ciphertext,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crypto_bigint::U256;

    fn sample_key() -> Sm2PrivateKey {
        let d =
            U256::from_be_hex("3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8");
        Sm2PrivateKey::from_scalar_inner(d).expect("valid d")
    }

    /// Unencrypted PKCS#8 round-trip.
    #[test]
    fn round_trip_unencrypted() {
        let key = sample_key();
        let der = encode(&key);
        let recovered = decode(&der).expect("decode");
        // Same scalar → same public key.
        assert!(bool::from(recovered.public_key().ct_eq(&key.public_key())));
    }

    /// Unencrypted decode rejects trailing junk.
    #[test]
    fn unencrypted_rejects_trailing_bytes() {
        let key = sample_key();
        let mut der = encode(&key);
        der.push(0x00);
        assert!(matches!(decode(&der), Err(Error::Failed)));
    }

    /// Unencrypted decode rejects mismatched optional public key.
    #[test]
    fn unencrypted_rejects_public_key_mismatch() {
        // Build a PKCS#8 by hand whose inner ECPrivateKey has the
        // SAMPLE-1 scalar but the SAMPLE-2 public key — must fail.
        let d1 =
            U256::from_be_hex("3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8");
        let d2 =
            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
        let key1 = Sm2PrivateKey::from_scalar_inner(d1).expect("d1");
        let key2 = Sm2PrivateKey::from_scalar_inner(d2).expect("d2");
        let scalar1 = key1.to_bytes_be();
        let pk2 = crate::sm2::Sm2PublicKey::from_point(key2.public_key()).to_sec1_uncompressed();
        let inner_bad = sec1::encode(&scalar1, Some(&pk2));

        // Wrap in unencrypted PKCS#8 manually (re-using what encode does).
        let mut alg_inner = Vec::new();
        writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
        writer::write_oid(&mut alg_inner, SM2P256V1);
        let mut alg_seq = Vec::new();
        writer::write_sequence(&mut alg_seq, &alg_inner);
        let mut body = Vec::new();
        writer::write_integer(&mut body, &[PKCS8_V1]);
        body.extend_from_slice(&alg_seq);
        writer::write_octet_string(&mut body, &inner_bad);
        let mut out = Vec::new();
        writer::write_sequence(&mut out, &body);

        assert!(matches!(decode(&out), Err(Error::Failed)));
    }

    /// Encrypted PKCS#8 round-trip with low iteration count.
    #[test]
    fn round_trip_encrypted() {
        let key = sample_key();
        let salt = [0xAB; 16];
        let iv = [0xCD; SM4_IV_LEN];
        let blob =
            encrypt(&key, b"correct horse battery staple", &salt, 1024, &iv).expect("encrypt");
        let recovered =
            decrypt(&blob, b"correct horse battery staple").expect("decrypt with right password");
        assert!(bool::from(recovered.public_key().ct_eq(&key.public_key())));
    }

    /// Encrypted decrypt with the wrong password fails into Failed.
    #[test]
    fn encrypted_wrong_password_fails() {
        let key = sample_key();
        let salt = [0xAB; 16];
        let iv = [0xCD; SM4_IV_LEN];
        let blob = encrypt(&key, b"right", &salt, 1024, &iv).expect("encrypt");
        assert!(matches!(decrypt(&blob, b"wrong"), Err(Error::Failed)));
    }

    #[test]
    fn encrypted_zero_iterations_rejected() {
        let key = sample_key();
        let salt = [0xAB; 16];
        let iv = [0xCD; SM4_IV_LEN];
        assert!(matches!(
            encrypt(&key, b"pw", &salt, 0, &iv),
            Err(Error::Failed)
        ));
    }

    #[test]
    fn decrypt_rejects_truncated_blob() {
        assert!(matches!(decrypt(&[], b"pw"), Err(Error::Failed)));
        assert!(matches!(decrypt(&[0x30, 0x00], b"pw"), Err(Error::Failed)));
    }

    #[test]
    fn decrypt_rejects_excessive_iterations() {
        // Build a malformed blob with iterations > PBKDF2_MAX_ITERATIONS.
        let key = sample_key();
        let salt = [0xAB; 16];
        let iv = [0xCD; SM4_IV_LEN];
        let blob = encrypt(&key, b"pw", &salt, 1024, &iv).expect("encrypt");
        // Patch the iteration-count INTEGER. The structure is
        // SEQ { SEQ { id-PBES2, SEQ { kdf, encScheme } }, OCTETSTRING }.
        // Easier: round-trip parse to confirm the gate, by wrapping
        // a hand-built blob with a forbidden iteration count.

        let bad_iter: u32 = PBKDF2_MAX_ITERATIONS + 1;
        let pbes2_params = build_pbes2_params(&salt, bad_iter, &iv);
        let mut alg_inner = Vec::new();
        writer::write_oid(&mut alg_inner, PBES2);
        alg_inner.extend_from_slice(&pbes2_params);
        let mut alg_seq = Vec::new();
        writer::write_sequence(&mut alg_seq, &alg_inner);
        // Borrow ciphertext bytes from the valid blob's tail.
        // Simplest: parse the valid blob to recover ciphertext slice.
        let parsed = parse_encrypted_blob(&blob).expect("baseline parse");
        let mut body = Vec::new();
        body.extend_from_slice(&alg_seq);
        writer::write_octet_string(&mut body, parsed.ciphertext);
        let mut bad_blob = Vec::new();
        writer::write_sequence(&mut bad_blob, &body);
        assert!(matches!(decrypt(&bad_blob, b"pw"), Err(Error::Failed)));
    }
}