Skip to content

[crypto/digital-certificate] Missing test coverage for special-char passwords — legacy PBE path untested + silent BMPString corruption for supplementary chars #23

@Yorizel

Description

@Yorizel

Summary

Audited @f-o-t/crypto (parsePkcs12) and @f-o-t/digital-certificate (parseCertificate) for password handling with special characters. Found three related gaps:

  1. No tests for passwords containing special characters (ASCII !@#$, accented é ã ç, emoji).
  2. Legacy PBE content-decryption path (3DES/PBE-SHA1-3DES) is completely untested — real Brazilian A1 ICP-Brasil certificates commonly use this format.
  3. passwordToBmpString silently produces invalid BMPString for passwords with characters outside Unicode BMP (U+10000+) — no error is raised, wrong key is derived.

Root Cause

1 — No test coverage for special-char passwords

All existing tests use ASCII-only passwords ("test1234", "test123"). The test fixtures:

digital-certificate/__tests__/fixtures/test-certificate.pfx → MAC: sha256, PBES2/PBKDF2/AES-256-CBC
e-signature/__tests__/fixtures/test.p12                     → same (modern OpenSSL default)

No test verifies that a password like "Sênh@123!" (common in Brazilian enterprise setups) is handled correctly.

2 — Legacy PBE content-decryption path is untested

parsePkcs12 has two content-decryption paths:

Path Code location Password encoding Tested?
PBES2 (AES-CBC + PBKDF2) decryptPbes2pbkdf2Sync(password, ...) UTF-8 (correct, OpenSSL-compatible) ✅ MAC only
Legacy PBE (3DES / RC2 + PKCS#12 KDF) decryptPbepkcs12KdfpasswordToBmpString UTF-16BE (correct per RFC 7292) ❌ Never

MAC verification always calls pkcs12Kdf (even for PBES2 PFX files), so passwordToBmpString is exercised — but only for MAC, not for content decryption. The 3DES decryptShroudedKeyBag path is never hit in any test.

Brazilian A1 certificates from ICP-Brasil CAs (the exact target of @f-o-t/digital-certificate) frequently use the legacy PBE-SHA1-3DES format — the path with zero coverage.

Verified via:

$ openssl pkcs12 -info -in __tests__/fixtures/test-certificate.pfx -password pass:test1234 -noout
MAC: sha256, Iteration 2048
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, ...  ← modern only
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, ...       ← modern only

3 — passwordToBmpString silently produces invalid BMPString for supplementary characters

File: libraries/crypto/src/pkcs12.ts:277

function passwordToBmpString(password: string): Uint8Array {
  const result = new Uint8Array(password.length * 2 + 2);
  for (let i = 0; i < password.length; i++) {
    const code = password.charCodeAt(i);   // ← returns UTF-16 code unit, not Unicode code point
    result[i * 2]     = (code >>> 8) & 0xff;
    result[i * 2 + 1] = code & 0xff;
  }
  return result;
}

For supplementary characters like 😀 (U+1F600), JavaScript represents them as two UTF-16 surrogate code units (0xD83D, 0xDE00). charCodeAt returns those raw surrogate values and the function encodes them as-is. BMPString (per X.680 §8.3) only allows code points U+0000–U+FFFF; surrogates are not valid BMPString values.

Consequence: A password containing emoji produces a silently incorrect KDF input. The MAC verification and/or key decryption will fail with a misleading "wrong password?" error rather than an actionable message. No validation is raised at the API boundary.

Note: For all BMP characters (U+0000–U+FFFF) — including common ASCII special chars (!@#$%^&*) and Latin Extended accented chars (é, ã, ç, ñ) — the function is correct. The bug only affects U+10000+ code points.


Affected Files

  • libraries/crypto/src/pkcs12.ts:277passwordToBmpString: no guard against supplementary characters
  • libraries/digital-certificate/__tests__/certificate.test.ts — all tests use ASCII-only password "test1234"
  • libraries/e-signature/__tests__/sign-pdf.test.ts — all tests use ASCII-only password "test123"
  • libraries/digital-certificate/__tests__/fixtures/test-certificate.pfx — PBES2/AES only; no legacy 3DES fixture

Expected Behavior

  • Passwords with common special characters (!@#$%^&*()_+, accented é ã ç) work correctly for both PBES2 and legacy PBE formats.
  • Passwords containing supplementary Unicode characters (U+10000+) raise a clear Pkcs12Error explaining that BMPString only supports BMP characters, rather than deriving a wrong key silently.
  • At least one test fixture uses legacy PBE-SHA1-3DES format so that decryptShroudedKeyBagdecryptPbepkcs12Kdf is covered end-to-end.

Actual Behavior

  • Special-char and non-ASCII passwords are untested — behavior is theoretically correct but unverified.
  • Legacy PBE content decryption path has no test; a regression could go undetected.
  • Passwords with emoji (or other supplementary chars) silently produce wrong key material and fail with a confusing "wrong password?" error.

Suggested Fix

A — Add validation in passwordToBmpString

// libraries/crypto/src/pkcs12.ts:277
function passwordToBmpString(password: string): Uint8Array {
  // Validate no supplementary characters (> U+FFFF) — BMPString restriction
  for (let i = 0; i < password.length; i++) {
    const code = password.charCodeAt(i);
    if (code >= 0xD800 && code <= 0xDFFF) {
      throw new Pkcs12Error(
        "Password contains a character outside the Unicode BMP (U+10000+). " +
        "PKCS#12 BMPString encoding only supports U+0000–U+FFFF."
      );
    }
  }
  // ... existing encoding ...
}

B — Add a legacy PBE test fixture

Generate a 3DES fixture:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=Test/O=Test"
openssl pkcs12 -export -legacy -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \
  -out test-legacy.pfx -inkey key.pem -in cert.pem -password pass:test1234

C — Add special-char password tests

// In both certificate.test.ts and sign-pdf.test.ts
it("parses PFX with special-character password", () => {
  // Generate fixture with: openssl pkcs12 ... -password pass:Sênh@123!
  const cert = parseCertificate(specialCharPfx, "Sênh@123!");
  expect(cert.certPem).toContain("-----BEGIN CERTIFICATE-----");
});

References

  • RFC 7292 Appendix B — PKCS#12 KDF password encoding (BMPString, UTF-16BE)
  • RFC 8018 §5.2 — PBKDF2 (no encoding mandated; OpenSSL uses UTF-8)
  • libraries/crypto/src/pkcs12.ts:205pkcs12Kdf (uses passwordToBmpString)
  • libraries/crypto/src/pkcs12.ts:438decryptPbes2 (uses raw string → UTF-8 via Node.js)
  • libraries/crypto/src/pkcs12.ts:549decryptShroudedKeyBag (legacy 3DES path, untested)

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions