Skip to main content

gmcrypto_core/
pkcs8.rs

1//! PKCS#8 `OneAsymmetricKey` codec (RFC 5958) + PBES2 encryption (RFC 8018).
2//!
3//! Wire shapes:
4//!
5//! ```text
6//! OneAsymmetricKey ::= SEQUENCE {
7//!     version              INTEGER (0),
8//!     privateKeyAlgorithm  AlgorithmIdentifier,
9//!     privateKey           OCTET STRING -- DER-encoded ECPrivateKey
10//! }
11//!
12//! EncryptedPrivateKeyInfo ::= SEQUENCE {
13//!     encryptionAlgorithm  AlgorithmIdentifier,
14//!     encryptedData        OCTET STRING
15//! }
16//! ```
17//!
18//! For SM2 the `privateKeyAlgorithm` is `id-ecPublicKey` with
19//! `namedCurve = sm2p256v1`; `privateKey` wraps a DER-encoded RFC 5915
20//! [`crate::sec1::EcPrivateKey`].
21//!
22//! For the encrypted variant, `encryptionAlgorithm` is `id-PBES2`
23//! with `keyDerivationFunc = id-PBKDF2` (PRF = id-hmacWithSM3) and
24//! `encryptionScheme = sm4-cbc` (IV in the parameters OCTET STRING).
25//! `encryptedData` is the SM4-CBC ciphertext of the inner
26//! `OneAsymmetricKey`.
27//!
28//! # Failure-mode invariant (API)
29//!
30//! All decoders return `Result<_, Error>` with a single
31//! [`Error::Failed`] variant. The return type carries no distinction
32//! between "wrong password", "malformed PEM", and "valid PEM but
33//! bad inner `ECPrivateKey`" — the caller sees one uninformative
34//! shape on any failure.
35//!
36//! # Timing-side-channel posture
37//!
38//! Code paths over **secret material** are constant-time-designed:
39//! PBKDF2-HMAC-SM3 (covered by [`crate::hmac`]'s constant-time
40//! discipline), SM4-CBC decrypt + PKCS#7 strip
41//! ([`crate::sm4::mode_cbc::decrypt`]), and the inner
42//! [`Sm2PrivateKey::from_bytes_be`] range gate. The W2 dudect target
43//! `ct_pkcs8_decrypt` class-splits by **password bytes** (both
44//! classes ship valid blobs so both succeed via identical control
45//! flow); local 10K-sample run measures `|tau| ≈ 0.02`.
46//!
47//! Code paths over **public attacker-supplied wire bytes** (the
48//! PBES2 structural parse) early-return on malformed input. A
49//! structurally invalid blob fails in microseconds; a structurally
50//! valid blob with a wrong password runs full PBKDF2 + SM4-CBC + an
51//! inner parse before failing. **This wall-clock distinction is
52//! observable but not secret-dependent** — the attacker built the
53//! blob and already knows its structural validity. The dudect
54//! gate above covers the only secret-dependent timing class
55//! (password vs password under a valid blob).
56//!
57//! # KDF parameter validation
58//!
59//! [`decrypt`] rejects `iterations == 0` (RFC 8018 §5.2 requires
60//! `c ≥ 1`) and `iterations > 10_000_000` (denial-of-service
61//! bound; the upper limit is per the W2 risk-trap in
62//! `docs/v0.3-scope.md`). Any other malformed PBES2 parameter
63//! folds into [`Error::Failed`] identically.
64
65use crate::asn1::oid::{ID_EC_PUBLIC_KEY, ID_HMAC_WITH_SM3, ID_PBKDF2, PBES2, SM2P256V1, SM4_CBC};
66use crate::asn1::{reader, writer};
67use crate::kdf::pbkdf2_hmac_sm3;
68use crate::sec1;
69use crate::sm2::Sm2PrivateKey;
70use crate::sm4::mode_cbc;
71use alloc::vec::Vec;
72use crypto_bigint::U256;
73use subtle::ConstantTimeEq;
74use zeroize::Zeroize;
75
76/// PKCS#8 codec failure — alias for the workspace-wide [`crate::Error`].
77///
78/// Single uninformative variant per the project's failure-mode
79/// invariant. Prior to v0.5 this was a distinct `pkcs8::Error` enum;
80/// v0.5 W5 unifies it with the workspace-wide type via this alias,
81/// so import paths and non-exhaustive `match` callsites against
82/// `pkcs8::Error::Failed` continue to work. **One caveat:** the
83/// workspace-wide type is `#[non_exhaustive]`, so downstream
84/// **exhaustive** `match` arms must now add a wildcard `_ => ...`
85/// (single-variant non-exhaustive enums require the wildcard from
86/// outside-crate matches).
87pub type Error = crate::Error;
88
89/// PKCS#8 version field (`v1 = 0`). RFC 5958 also defines `v2 = 1`
90/// when the optional `publicKey` BIT STRING is present; v0.3 emits
91/// `v1` and accepts both `v1` and `v2`.
92const PKCS8_V1: u8 = 0;
93const PKCS8_V2: u8 = 1;
94
95/// Default SM4-CBC key length (always 16 bytes for SM4).
96const SM4_KEY_LEN: usize = 16;
97/// SM4 block size = CBC IV size.
98const SM4_IV_LEN: usize = 16;
99
100/// PBKDF2 iteration count upper bound for [`decrypt`].
101///
102/// Rejects adversarial blobs that would burn unbounded CPU. v0.3
103/// picks `10_000_000` as the denial-of-service ceiling — well
104/// above any realistic production iteration count and still
105/// bounded.
106pub const PBKDF2_MAX_ITERATIONS: u32 = 10_000_000;
107
108/// Encode an SM2 private key as a DER-encoded **unencrypted**
109/// PKCS#8 `OneAsymmetricKey`.
110///
111/// The inner `ECPrivateKey` carries the scalar plus the optional
112/// `publicKey` field (uncompressed `04 || X || Y`); the outer
113/// privateKeyAlgorithm is `id-ecPublicKey` with `sm2p256v1`.
114///
115/// The intermediate `ECPrivateKey` body is zeroized before return.
116/// **The returned `Vec<u8>` contains the raw scalar bytes** —
117/// caller is responsible for wiping it before letting it leave
118/// their stack frame.
119#[must_use]
120pub fn encode(key: &Sm2PrivateKey) -> Vec<u8> {
121    let mut scalar_be = key.to_bytes_be();
122    let pub_uncompressed = {
123        let pub_key = crate::sm2::Sm2PublicKey::from_point(key.public_key());
124        pub_key.to_sec1_uncompressed()
125    };
126    let mut inner = sec1::encode(&scalar_be, Some(&pub_uncompressed));
127    scalar_be.zeroize();
128
129    // privateKeyAlgorithm SEQUENCE { id-ecPublicKey, namedCurve }
130    let mut alg_inner = Vec::with_capacity(ID_EC_PUBLIC_KEY.len() + SM2P256V1.len() + 4);
131    writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
132    writer::write_oid(&mut alg_inner, SM2P256V1);
133    let mut alg_seq = Vec::with_capacity(alg_inner.len() + 4);
134    writer::write_sequence(&mut alg_seq, &alg_inner);
135
136    let mut body = Vec::with_capacity(inner.len() + alg_seq.len() + 8);
137    writer::write_integer(&mut body, &[PKCS8_V1]);
138    body.extend_from_slice(&alg_seq);
139    writer::write_octet_string(&mut body, &inner);
140
141    let mut out = Vec::with_capacity(body.len() + 4);
142    writer::write_sequence(&mut out, &body);
143
144    // Wipe the secret-bearing intermediates. (Zeroize is a side-
145    // effecting write the optimizer must preserve via the
146    // `volatile_write` in `zeroize::Zeroize`; the buffer is read by
147    // SM4-CBC during the encrypt path's previous use.)
148    inner.zeroize();
149    body.zeroize();
150    out
151}
152
153/// Decode an unencrypted PKCS#8 `OneAsymmetricKey` blob into an
154/// [`Sm2PrivateKey`].
155///
156/// Validates the version (0 or 1), the privateKeyAlgorithm
157/// (id-ecPublicKey + sm2p256v1), the inner `ECPrivateKey` scalar
158/// (`d ∈ [1, n-2]`), and any optional public key (must match
159/// `d·G`).
160///
161/// # Errors
162///
163/// Returns [`Error::Failed`] for any malformed input or
164/// out-of-range scalar.
165pub fn decode(input: &[u8]) -> Result<Sm2PrivateKey, Error> {
166    let (body, rest) = reader::read_sequence(input).ok_or(Error::Failed)?;
167    if !rest.is_empty() {
168        return Err(Error::Failed);
169    }
170
171    // version INTEGER
172    let (version, body) = reader::read_integer(body).ok_or(Error::Failed)?;
173    if version != [PKCS8_V1] && version != [PKCS8_V2] {
174        return Err(Error::Failed);
175    }
176
177    // privateKeyAlgorithm SEQUENCE
178    let (alg_inner, body) = reader::read_sequence(body).ok_or(Error::Failed)?;
179    let (alg_oid, alg_inner) = reader::read_oid(alg_inner).ok_or(Error::Failed)?;
180    if alg_oid != ID_EC_PUBLIC_KEY {
181        return Err(Error::Failed);
182    }
183    let (curve_oid, alg_inner) = reader::read_oid(alg_inner).ok_or(Error::Failed)?;
184    if curve_oid != SM2P256V1 || !alg_inner.is_empty() {
185        return Err(Error::Failed);
186    }
187
188    // privateKey OCTET STRING { ECPrivateKey }
189    let (inner_bytes, body) = reader::read_octet_string(body).ok_or(Error::Failed)?;
190    let mut inner = sec1::decode(inner_bytes).ok_or(Error::Failed)?;
191
192    // Trailing PKCS#8 v2 attributes/publicKey are tolerated but not
193    // required; we reject any unrecognized tag to stay strict.
194    // (gmssl 3.1.1 emits PKCS#8 v1 by default, which has no trailing
195    // fields.)
196    if !body.is_empty() {
197        // PKCS#8 v2 may carry [0] Attributes and [1] BIT STRING
198        // publicKey. Skip them tolerantly: we don't need either,
199        // since the inner ECPrivateKey already carries the public
200        // point and the scalar is authoritative.
201        let mut tail = body;
202        while !tail.is_empty() {
203            // Try [0] attributes EXPLICIT.
204            if let Some((_, after)) = reader::read_context_tagged_explicit(tail, 0) {
205                tail = after;
206                continue;
207            }
208            // Try [1] publicKey EXPLICIT BIT STRING.
209            if let Some((_, after)) = reader::read_context_tagged_explicit(tail, 1) {
210                tail = after;
211                continue;
212            }
213            // Unknown tag — fold into Failed.
214            inner.scalar_be.zeroize();
215            return Err(Error::Failed);
216        }
217    }
218
219    let d = U256::from_be_slice(&inner.scalar_be);
220    inner.scalar_be.zeroize();
221    let key = Sm2PrivateKey::from_scalar_inner(d);
222    let key: Option<Sm2PrivateKey> = key.into();
223    let key = key.ok_or(Error::Failed)?;
224
225    // If the inner ECPrivateKey carried a publicKey, cross-check it
226    // matches d·G — defends against stripped-public-key + stripped-
227    // scalar swaps that escape unencrypted PKCS#8 detection.
228    if let Some(stored_pub) = inner.public {
229        let derived = key.public_key();
230        if !bool::from(stored_pub.ct_eq(&derived)) {
231            return Err(Error::Failed);
232        }
233    }
234
235    Ok(key)
236}
237
238/// Encrypt an SM2 private key as a DER-encoded RFC 5958
239/// `EncryptedPrivateKeyInfo` blob using PBES2 (PBKDF2-HMAC-SM3 +
240/// SM4-CBC).
241///
242/// `salt` and `iv` must be **caller-supplied unpredictable
243/// CSPRNG output** — see `CLAUDE.md` and
244/// [`crate::sm4::mode_cbc::encrypt`]'s IV contract. Re-using the
245/// same `(salt, iv)` under the same password under two different
246/// keys is a key-recovery attack.
247///
248/// `iterations` should be at least 600,000 (OWASP 2024 PBKDF2
249/// baseline); v0.3 does not enforce a minimum to keep test
250/// vectors reproducible cheaply, but production callers must
251/// pick.
252///
253/// # Errors
254///
255/// Returns [`Error::Failed`] only if `iterations == 0` (RFC 8018
256/// `c ≥ 1`).
257pub fn encrypt(
258    key: &Sm2PrivateKey,
259    password: &[u8],
260    salt: &[u8],
261    iterations: u32,
262    iv: &[u8; SM4_IV_LEN],
263) -> Result<Vec<u8>, Error> {
264    if iterations == 0 {
265        return Err(Error::Failed);
266    }
267
268    // 1. Encode the inner OneAsymmetricKey.
269    let mut inner = encode(key);
270
271    // 2. Derive an SM4-CBC key: PBKDF2-HMAC-SM3(password, salt, iter, 16).
272    let mut sm4_key = [0u8; SM4_KEY_LEN];
273    pbkdf2_hmac_sm3(password, salt, iterations, &mut sm4_key).ok_or(Error::Failed)?;
274
275    // 3. Encrypt under SM4-CBC + PKCS#7 padding.
276    let ciphertext = mode_cbc::encrypt(&sm4_key, iv, &inner);
277
278    // Wipe sensitive intermediates.
279    inner.zeroize();
280    sm4_key.zeroize();
281
282    // 4. Build the EncryptedPrivateKeyInfo wrapper.
283    let pbes2_params = build_pbes2_params(salt, iterations, iv);
284    let mut alg_inner = Vec::with_capacity(PBES2.len() + pbes2_params.len() + 4);
285    writer::write_oid(&mut alg_inner, PBES2);
286    alg_inner.extend_from_slice(&pbes2_params);
287    let mut alg_seq = Vec::with_capacity(alg_inner.len() + 4);
288    writer::write_sequence(&mut alg_seq, &alg_inner);
289
290    let mut body = Vec::with_capacity(alg_seq.len() + ciphertext.len() + 8);
291    body.extend_from_slice(&alg_seq);
292    writer::write_octet_string(&mut body, &ciphertext);
293
294    let mut out = Vec::with_capacity(body.len() + 4);
295    writer::write_sequence(&mut out, &body);
296    Ok(out)
297}
298
299/// Build the PBES2 `parameters` SEQUENCE for [`encrypt`].
300///
301/// ```text
302/// PBES2-params ::= SEQUENCE {
303///     keyDerivationFunc  AlgorithmIdentifier { id-PBKDF2,  PBKDF2-params },
304///     encryptionScheme   AlgorithmIdentifier { sm4-cbc,    OCTET STRING (IV) }
305/// }
306///
307/// PBKDF2-params ::= SEQUENCE {
308///     salt            OCTET STRING,
309///     iterationCount  INTEGER,
310///     keyLength       INTEGER OPTIONAL,
311///     prf             AlgorithmIdentifier { id-hmacWithSM3, NULL }
312/// }
313/// ```
314fn build_pbes2_params(salt: &[u8], iterations: u32, iv: &[u8; SM4_IV_LEN]) -> Vec<u8> {
315    // PBKDF2-params.
316    let mut pbkdf2_inner = Vec::with_capacity(salt.len() + 32);
317    writer::write_octet_string(&mut pbkdf2_inner, salt);
318    writer::write_integer(&mut pbkdf2_inner, &iterations.to_be_bytes());
319    // PRF AlgorithmIdentifier { id-hmacWithSM3, NULL }
320    let mut prf_inner = Vec::with_capacity(ID_HMAC_WITH_SM3.len() + 4);
321    writer::write_oid(&mut prf_inner, ID_HMAC_WITH_SM3);
322    writer::write_null(&mut prf_inner);
323    let mut prf_seq = Vec::with_capacity(prf_inner.len() + 4);
324    writer::write_sequence(&mut prf_seq, &prf_inner);
325    pbkdf2_inner.extend_from_slice(&prf_seq);
326
327    let mut pbkdf2_seq = Vec::with_capacity(pbkdf2_inner.len() + 4);
328    writer::write_sequence(&mut pbkdf2_seq, &pbkdf2_inner);
329
330    // keyDerivationFunc AlgorithmIdentifier { id-PBKDF2, PBKDF2-params }.
331    let mut kdf_inner = Vec::with_capacity(ID_PBKDF2.len() + pbkdf2_seq.len() + 4);
332    writer::write_oid(&mut kdf_inner, ID_PBKDF2);
333    kdf_inner.extend_from_slice(&pbkdf2_seq);
334    let mut kdf_seq = Vec::with_capacity(kdf_inner.len() + 4);
335    writer::write_sequence(&mut kdf_seq, &kdf_inner);
336
337    // encryptionScheme AlgorithmIdentifier { sm4-cbc, OCTET STRING (IV) }.
338    let mut es_inner = Vec::with_capacity(SM4_CBC.len() + iv.len() + 4);
339    writer::write_oid(&mut es_inner, SM4_CBC);
340    writer::write_octet_string(&mut es_inner, iv);
341    let mut es_seq = Vec::with_capacity(es_inner.len() + 4);
342    writer::write_sequence(&mut es_seq, &es_inner);
343
344    let mut params_inner = Vec::with_capacity(kdf_seq.len() + es_seq.len());
345    params_inner.extend_from_slice(&kdf_seq);
346    params_inner.extend_from_slice(&es_seq);
347
348    let mut out = Vec::with_capacity(params_inner.len() + 4);
349    writer::write_sequence(&mut out, &params_inner);
350    out
351}
352
353/// Decrypt a DER-encoded RFC 5958 `EncryptedPrivateKeyInfo` blob.
354///
355/// Parses the PBES2 parameters, derives the SM4-CBC key from
356/// `password`, decrypts, and reconstructs the inner SM2 private
357/// key. Single uninformative outcome — no path to distinguish
358/// "wrong password" from "malformed PBES2 parameters" from
359/// "decrypt succeeded but inner key was malformed".
360///
361/// # Errors
362///
363/// Returns [`Error::Failed`] for any malformed input. Same shape
364/// regardless of where the failure occurred.
365pub fn decrypt(input: &[u8], password: &[u8]) -> Result<Sm2PrivateKey, Error> {
366    let parsed = parse_encrypted_blob(input).ok_or(Error::Failed)?;
367
368    // Derive the SM4-CBC key from password + salt + iterations.
369    let mut sm4_key = [0u8; SM4_KEY_LEN];
370    let derive_ok =
371        pbkdf2_hmac_sm3(password, parsed.salt, parsed.iterations, &mut sm4_key).is_some();
372    // Even if derive failed (iterations == 0 caught above by the
373    // 1..=PBKDF2_MAX_ITERATIONS gate, so this is unreachable in
374    // practice), continue through decrypt with a zero key to keep
375    // failure-mode shapes uniform.
376    let _ = derive_ok;
377
378    // SM4-CBC decrypt + PKCS#7 strip. Folds into Failed on any
379    // failure (length not multiple of 16, bad pad, etc.).
380    let plaintext = mode_cbc::decrypt(&sm4_key, &parsed.iv, parsed.ciphertext);
381    sm4_key.zeroize();
382
383    let mut plaintext = plaintext.ok_or(Error::Failed)?;
384
385    // Parse the inner unencrypted OneAsymmetricKey. Failure folds
386    // into the same Failed.
387    let result = decode(&plaintext);
388    plaintext.zeroize();
389    result
390}
391
392/// Internal: parsed PBES2 parameters + ciphertext.
393struct ParsedEncrypted<'a> {
394    salt: &'a [u8],
395    iterations: u32,
396    iv: [u8; SM4_IV_LEN],
397    ciphertext: &'a [u8],
398}
399
400/// Parse an `EncryptedPrivateKeyInfo` blob into validated PBES2
401/// parameters + the ciphertext slice. Returns `None` for any
402/// malformed input or unsupported algorithm.
403fn parse_encrypted_blob(input: &[u8]) -> Option<ParsedEncrypted<'_>> {
404    let (body, rest) = reader::read_sequence(input)?;
405    if !rest.is_empty() {
406        return None;
407    }
408    // encryptionAlgorithm SEQUENCE { id-PBES2, PBES2-params }.
409    let (alg_inner, body) = reader::read_sequence(body)?;
410    let (alg_oid, alg_inner) = reader::read_oid(alg_inner)?;
411    if alg_oid != PBES2 {
412        return None;
413    }
414    let (params_inner, alg_inner_rest) = reader::read_sequence(alg_inner)?;
415    if !alg_inner_rest.is_empty() {
416        return None;
417    }
418
419    // keyDerivationFunc SEQUENCE { id-PBKDF2, PBKDF2-params }.
420    let (kdf_seq, params_rest) = reader::read_sequence(params_inner)?;
421    let (kdf_oid, kdf_after) = reader::read_oid(kdf_seq)?;
422    if kdf_oid != ID_PBKDF2 {
423        return None;
424    }
425    let (pbkdf2_inner, kdf_seq_rest) = reader::read_sequence(kdf_after)?;
426    if !kdf_seq_rest.is_empty() {
427        return None;
428    }
429
430    // PBKDF2-params: salt, iterations, [keyLength], [PRF].
431    let (salt, pbkdf2_inner) = reader::read_octet_string(pbkdf2_inner)?;
432    let (iter_bytes, mut pbkdf2_inner) = reader::read_integer(pbkdf2_inner)?;
433    if iter_bytes.len() > 4 {
434        return None;
435    }
436    let mut iter_buf = [0u8; 4];
437    iter_buf[4 - iter_bytes.len()..].copy_from_slice(iter_bytes);
438    let iterations = u32::from_be_bytes(iter_buf);
439    if iterations == 0 || iterations > PBKDF2_MAX_ITERATIONS {
440        return None;
441    }
442    // Optional keyLength. SM4-CBC fixes 16 bytes; if present, must equal 16.
443    if let Some((kl_bytes, after)) = reader::read_integer(pbkdf2_inner) {
444        if kl_bytes.len() > 4 {
445            return None;
446        }
447        let mut kl_buf = [0u8; 4];
448        kl_buf[4 - kl_bytes.len()..].copy_from_slice(kl_bytes);
449        let key_length = u32::from_be_bytes(kl_buf) as usize;
450        if key_length != SM4_KEY_LEN {
451            return None;
452        }
453        pbkdf2_inner = after;
454    }
455    // Optional PRF. Default is HMAC-SHA-1 per RFC 8018; we accept
456    // only id-hmacWithSM3 (gmssl convention; required for SM2 PKCS#8).
457    // No PRF specified would default to HMAC-SHA1, which is invalid
458    // for SM2 PKCS#8 — reject.
459    if pbkdf2_inner.is_empty() {
460        return None;
461    }
462    let (prf_seq, prf_rest) = reader::read_sequence(pbkdf2_inner)?;
463    if !prf_rest.is_empty() {
464        return None;
465    }
466    let (prf_oid, prf_seq_rest) = reader::read_oid(prf_seq)?;
467    if prf_oid != ID_HMAC_WITH_SM3 {
468        return None;
469    }
470    // The PRF parameters MAY be NULL or absent.
471    if !prf_seq_rest.is_empty()
472        && (reader::read_null(prf_seq_rest).is_none() || prf_seq_rest.len() != 2)
473    {
474        return None;
475    }
476
477    // encryptionScheme SEQUENCE { sm4-cbc, OCTET STRING (IV) }.
478    let (es_seq, params_outer_rest) = reader::read_sequence(params_rest)?;
479    if !params_outer_rest.is_empty() {
480        return None;
481    }
482    let (es_oid, es_after) = reader::read_oid(es_seq)?;
483    if es_oid != SM4_CBC {
484        return None;
485    }
486    let (iv_bytes, es_seq_rest) = reader::read_octet_string(es_after)?;
487    if !es_seq_rest.is_empty() || iv_bytes.len() != SM4_IV_LEN {
488        return None;
489    }
490    let mut iv = [0u8; SM4_IV_LEN];
491    iv.copy_from_slice(iv_bytes);
492
493    // encryptedData OCTET STRING.
494    let (ciphertext, body_rest) = reader::read_octet_string(body)?;
495    if !body_rest.is_empty() {
496        return None;
497    }
498
499    Some(ParsedEncrypted {
500        salt,
501        iterations,
502        iv,
503        ciphertext,
504    })
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use crypto_bigint::U256;
511
512    fn sample_key() -> Sm2PrivateKey {
513        let d =
514            U256::from_be_hex("3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8");
515        Sm2PrivateKey::from_scalar_inner(d).expect("valid d")
516    }
517
518    /// Unencrypted PKCS#8 round-trip.
519    #[test]
520    fn round_trip_unencrypted() {
521        let key = sample_key();
522        let der = encode(&key);
523        let recovered = decode(&der).expect("decode");
524        // Same scalar → same public key.
525        assert!(bool::from(recovered.public_key().ct_eq(&key.public_key())));
526    }
527
528    /// Unencrypted decode rejects trailing junk.
529    #[test]
530    fn unencrypted_rejects_trailing_bytes() {
531        let key = sample_key();
532        let mut der = encode(&key);
533        der.push(0x00);
534        assert!(matches!(decode(&der), Err(Error::Failed)));
535    }
536
537    /// Unencrypted decode rejects mismatched optional public key.
538    #[test]
539    fn unencrypted_rejects_public_key_mismatch() {
540        // Build a PKCS#8 by hand whose inner ECPrivateKey has the
541        // SAMPLE-1 scalar but the SAMPLE-2 public key — must fail.
542        let d1 =
543            U256::from_be_hex("3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8");
544        let d2 =
545            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
546        let key1 = Sm2PrivateKey::from_scalar_inner(d1).expect("d1");
547        let key2 = Sm2PrivateKey::from_scalar_inner(d2).expect("d2");
548        let scalar1 = key1.to_bytes_be();
549        let pk2 = crate::sm2::Sm2PublicKey::from_point(key2.public_key()).to_sec1_uncompressed();
550        let inner_bad = sec1::encode(&scalar1, Some(&pk2));
551
552        // Wrap in unencrypted PKCS#8 manually (re-using what encode does).
553        let mut alg_inner = Vec::new();
554        writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
555        writer::write_oid(&mut alg_inner, SM2P256V1);
556        let mut alg_seq = Vec::new();
557        writer::write_sequence(&mut alg_seq, &alg_inner);
558        let mut body = Vec::new();
559        writer::write_integer(&mut body, &[PKCS8_V1]);
560        body.extend_from_slice(&alg_seq);
561        writer::write_octet_string(&mut body, &inner_bad);
562        let mut out = Vec::new();
563        writer::write_sequence(&mut out, &body);
564
565        assert!(matches!(decode(&out), Err(Error::Failed)));
566    }
567
568    /// Encrypted PKCS#8 round-trip with low iteration count.
569    #[test]
570    fn round_trip_encrypted() {
571        let key = sample_key();
572        let salt = [0xAB; 16];
573        let iv = [0xCD; SM4_IV_LEN];
574        let blob =
575            encrypt(&key, b"correct horse battery staple", &salt, 1024, &iv).expect("encrypt");
576        let recovered =
577            decrypt(&blob, b"correct horse battery staple").expect("decrypt with right password");
578        assert!(bool::from(recovered.public_key().ct_eq(&key.public_key())));
579    }
580
581    /// Encrypted decrypt with the wrong password fails into Failed.
582    #[test]
583    fn encrypted_wrong_password_fails() {
584        let key = sample_key();
585        let salt = [0xAB; 16];
586        let iv = [0xCD; SM4_IV_LEN];
587        let blob = encrypt(&key, b"right", &salt, 1024, &iv).expect("encrypt");
588        assert!(matches!(decrypt(&blob, b"wrong"), Err(Error::Failed)));
589    }
590
591    #[test]
592    fn encrypted_zero_iterations_rejected() {
593        let key = sample_key();
594        let salt = [0xAB; 16];
595        let iv = [0xCD; SM4_IV_LEN];
596        assert!(matches!(
597            encrypt(&key, b"pw", &salt, 0, &iv),
598            Err(Error::Failed)
599        ));
600    }
601
602    #[test]
603    fn decrypt_rejects_truncated_blob() {
604        assert!(matches!(decrypt(&[], b"pw"), Err(Error::Failed)));
605        assert!(matches!(decrypt(&[0x30, 0x00], b"pw"), Err(Error::Failed)));
606    }
607
608    #[test]
609    fn decrypt_rejects_excessive_iterations() {
610        // Build a malformed blob with iterations > PBKDF2_MAX_ITERATIONS.
611        let key = sample_key();
612        let salt = [0xAB; 16];
613        let iv = [0xCD; SM4_IV_LEN];
614        let blob = encrypt(&key, b"pw", &salt, 1024, &iv).expect("encrypt");
615        // Patch the iteration-count INTEGER. The structure is
616        // SEQ { SEQ { id-PBES2, SEQ { kdf, encScheme } }, OCTETSTRING }.
617        // Easier: round-trip parse to confirm the gate, by wrapping
618        // a hand-built blob with a forbidden iteration count.
619
620        let bad_iter: u32 = PBKDF2_MAX_ITERATIONS + 1;
621        let pbes2_params = build_pbes2_params(&salt, bad_iter, &iv);
622        let mut alg_inner = Vec::new();
623        writer::write_oid(&mut alg_inner, PBES2);
624        alg_inner.extend_from_slice(&pbes2_params);
625        let mut alg_seq = Vec::new();
626        writer::write_sequence(&mut alg_seq, &alg_inner);
627        // Borrow ciphertext bytes from the valid blob's tail.
628        // Simplest: parse the valid blob to recover ciphertext slice.
629        let parsed = parse_encrypted_blob(&blob).expect("baseline parse");
630        let mut body = Vec::new();
631        body.extend_from_slice(&alg_seq);
632        writer::write_octet_string(&mut body, parsed.ciphertext);
633        let mut bad_blob = Vec::new();
634        writer::write_sequence(&mut bad_blob, &body);
635        assert!(matches!(decrypt(&bad_blob, b"pw"), Err(Error::Failed)));
636    }
637}