X Tutup
Skip to content

Accept Base64 format with trailing whitespace#409

Open
mikkosuonio wants to merge 3 commits intonode-saml:masterfrom
mikkosuonio:mikkosuonio/issue-361-fix-pr
Open

Accept Base64 format with trailing whitespace#409
mikkosuonio wants to merge 3 commits intonode-saml:masterfrom
mikkosuonio:mikkosuonio/issue-361-fix-pr

Conversation

@mikkosuonio
Copy link

@mikkosuonio mikkosuonio commented Sep 18, 2025

This adds support more liberal use of newline and whitespace in Base64 encoded certificates and keys (used without the encapsulation boundaries of the PEM format). This was initially reported as #361.

This should enable the use of common Base64 encoding tools, which may add trailing whitespace.

This is based on RFC7468, especially section 3 and other remarks on whitespace.

This fix does not trim the whitespace for the generated PEM format, but can be easily extended to that.

Summary by CodeRabbit

  • Refactor

    • Improved input normalization for key-to-PEM conversion to more robustly handle string vs. binary inputs while keeping the public API unchanged.
  • Tests

    • Added tests ensuring Base64 key inputs with trailing whitespace/newlines and varying padding are accepted and that PEM output preserves the original trailing whitespace inside the certificate body.

@coderabbitai
Copy link

coderabbitai bot commented Sep 18, 2025

Walkthrough

Adds a private helper bufferToString in src/crypto.ts to normalize Buffer/string inputs (using latin1) and trims for validation; keyInfoToPem uses it. Tests in test/crypto.spec.ts were added to verify Base64 inputs with padding and trailing whitespace are accepted and that the PEM body preserves that whitespace.

Changes

Cohort / File(s) Summary
Crypto helper & key parsing
src/crypto.ts
Introduced internal bufferToString(keyInfo) to convert Buffers to latin1 strings; keyInfoToPem now calls this helper and trims for validation while preserving trimmed content in PEM construction. Public signatures unchanged.
Base64 whitespace tests
test/crypto.spec.ts
Added tests asserting keyInfoToPem accepts Base64 strings with 0–2 padding chars and trailing whitespace, and that the resulting PEM body retains trailing whitespace.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant keyInfoToPem
    participant bufferToString
    participant Validator
    participant PEMBuilder

    Caller->>keyInfoToPem: call(keyInfo, pemLabel, optionName)
    keyInfoToPem->>bufferToString: bufferToString(keyInfo)
    bufferToString-->>keyInfoToPem: string keyData
    alt keyData looks like PEM
        keyInfoToPem->>Validator: validate PEM (header/footer)
        Validator-->>keyInfoToPem: valid
        keyInfoToPem->>PEMBuilder: normalize PEM (uses keyData)
    else keyData looks like Base64
        keyInfoToPem->>Validator: validate Base64 (allow padding + trailing whitespace)
        Validator-->>keyInfoToPem: valid
        keyInfoToPem->>PEMBuilder: build PEM body (preserve trailing whitespace in body)
    end
    PEMBuilder-->>keyInfoToPem: pemString
    keyInfoToPem-->>Caller: pemString
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Pay attention to bufferToString encoding choice (latin1) and trimming behavior.
  • Verify PEM construction uses the trimmed keyData consistently and tests cover edge whitespace cases.

Poem

A nibble of keys, a trim of the byte,
I hop through buffers, keeping things right.
Trailing spaces welcome, padding too,
Into the PEM I tidy and strew.
Thump—tests pass! A carrot for you.

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "Accept Base64 format with trailing whitespace" is directly related to the main objective of this PR, which is to add support for Base64-encoded certificates and keys with trailing whitespace. The title is concise, clear, and specific enough that a teammate reviewing the commit history would immediately understand the primary change without needing to read the full description. The title accurately reflects the core feature being implemented and passes the criterion of being fully related to the main change in the changeset.
Description Check ✅ Passed The PR description contains all the essential required information: it provides a clear summary of the change (support for Base64 with trailing whitespace), identifies the related issue (#361), explains the use-case (common Base64 encoding tools may add trailing whitespace), and includes a reference to RFC 7468 section 3 with a direct link. The description is well-structured, on-topic, and substantive. While the checklist items at the end of the template are not explicitly marked as completed, the raw summary confirms that tests were indeed added to the PR, and the description adequately addresses the required template sections.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/crypto.ts (3)

59-61: Avoid TypeError when invalid types slip through; trim only after ensuring a string

bufferToString(keyInfo)?.trim() can throw if keyInfo is a non‑string/non‑Buffer at runtime (e.g., the test that passes false) because value?.trim() still attempts to call an undefined trim function on booleans. Make bufferToString defensively return a string and then trim that.

Apply this diff:

-  const keyData = bufferToString(keyInfo)?.trim();
+  const rawKeyData = bufferToString(keyInfo);
+  const keyData = rawKeyData.trim();

69-71: Use consistent encoding when embedding the body into PEM

Interpolating a Buffer into a template string uses UTF‑8 by default. For Base64 we want a byte‑preserving conversion (latin1/ASCII). Reuse the helper.

Apply this diff:

-  const pem = `-----BEGIN ${pemLabel}-----\n${keyInfo}\n-----END ${pemLabel}-----`;
+  const pemBody = bufferToString(keyInfo);
+  const pem = `-----BEGIN ${pemLabel}-----\n${pemBody}\n-----END ${pemLabel}-----`;

74-76: Hoist and harden helper to eliminate TDZ risks and handle unexpected inputs

Arrow functions aren’t hoisted; while safe today, converting to a function declaration removes any future ordering pitfalls. Also return a string for any input to keep callers simple and safe.

Apply this diff:

-const bufferToString = (keyInfo: string | Buffer) => {
-  return Buffer.isBuffer(keyInfo) ? keyInfo.toString("latin1") : keyInfo;
-}
+function bufferToString(keyInfo: string | Buffer): string {
+  if (Buffer.isBuffer(keyInfo)) return keyInfo.toString("latin1");
+  if (typeof keyInfo === "string") return keyInfo;
+  // Defensive: unexpected runtime inputs
+  return "";
+}
test/crypto.spec.ts (1)

152-166: Add a couple more cases to lock behavior down (optional)

  • Buffer inputs with trailing whitespace (space/tab)
  • CRLF endings with trailing space per line

Example additions:

it("accepts Buffer Base64 with trailing space", function () {
  const certificate = keyInfoToPem(Buffer.from("QUIK \n", "latin1"), "CERTIFICATE");
  expect(certificate).to.equal("-----BEGIN CERTIFICATE-----\nQUIK \n-----END CERTIFICATE-----\n");
});

it("accepts Base64 with trailing tab and CRLF", function () {
  const certificate = keyInfoToPem("QQo=\t\r\n", "CERTIFICATE");
  expect(certificate).to.equal("-----BEGIN CERTIFICATE-----\nQQo=\t\r\n-----END CERTIFICATE-----\n");
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25c434a and fedf665.

📒 Files selected for processing (2)
  • src/crypto.ts (2 hunks)
  • test/crypto.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/crypto.spec.ts (1)
src/crypto.ts (1)
  • keyInfoToPem (54-72)
🔇 Additional comments (3)
test/crypto.spec.ts (3)

153-156: LGTM: accepts Base64 without padding + trailing whitespace

This validates the intended relaxation and preserves whitespace inside the PEM body.


158-161: LGTM: accepts single '=' padding + trailing whitespace

Covers a common encoder output variant.


163-166: LGTM: accepts '==' padding + trailing whitespace

Good coverage of padding cases with whitespace.

This should ensure 'latin1' encoding is used for keyInfo of type
Buffer.
@mikkosuonio mikkosuonio force-pushed the mikkosuonio/issue-361-fix-pr branch from fedf665 to 1b39a96 Compare November 3, 2025 07:14
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fedf665 and 1b39a96.

📒 Files selected for processing (2)
  • src/crypto.ts (3 hunks)
  • test/crypto.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/crypto.spec.ts (1)
src/crypto.ts (1)
  • keyInfoToPem (58-76)
🔇 Additional comments (1)
src/crypto.ts (1)

51-53: Verify latin1 encoding choice for PEM/Base64 data.

The bufferToString helper uses latin1 encoding when converting Buffers. While this works correctly for Base64/PEM data (which uses ASCII characters), the default toString() encoding is utf8.

Since Base64 and PEM formats only use ASCII characters (subset of both UTF-8 and latin1), both encodings would produce identical results for valid inputs. However, for invalid inputs or edge cases, the behavior might differ.

Consider either:

  1. Using utf8 (default) for consistency with common Node.js patterns
  2. Adding a comment explaining why latin1 was chosen for this use case

If latin1 was chosen to handle potentially malformed data as raw bytes, please add a comment documenting this rationale.

optionName = "keyInfo",
): string => {
const keyData = Buffer.isBuffer(keyInfo) ? keyInfo.toString("latin1") : keyInfo;
const keyData = bufferToString(keyInfo)?.trim();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Implementation correctly trims whitespace, but conflicts with PR description and test expectations.

The change to use bufferToString(keyInfo)?.trim() successfully achieves the goal of accepting Base64 inputs with trailing whitespace by trimming before validation. The trimmed keyData is then used in PEM construction (line 73) and normalized via normalizePemFile, which is correct behavior per RFC 7468.

However, there's an inconsistency with:

  1. PR description: States "The fix does not trim whitespace for generated PEM output" — but the implementation DOES trim (line 63) and normalizes the output.
  2. Test expectations in test/crypto.spec.ts (lines 153-172): Expect trailing spaces to be preserved in the output, but trim() removes them.

The implementation approach (trim input, normalize output) is the correct interpretation of RFC 7468, which advocates for liberal input parsing but strict output formatting. The confusion seems to stem from the PR description and test expectations.

Recommendation: Keep the implementation as-is, but update:

  1. The PR description to clarify that trailing whitespace is trimmed from output (not preserved)
  2. The test expectations to match the actual behavior (see test file review comments)

Also applies to: 73-73

🤖 Prompt for AI Agents
In src/crypto.ts around line 63 (and related behavior at line 73), the code
trims whitespace from Base64 input which is correct per RFC 7468 but conflicts
with the PR description and tests; keep the implementation as-is (trim input,
normalize output), update the PR description to state that trailing whitespace
is trimmed and not preserved in generated PEM output, and update
test/crypto.spec.ts (tests around lines 153-172) to expect trimmed/normalized
PEM output instead of preserving trailing spaces.

Comment on lines +153 to +172
it("should allow Base64 without padding but with trailing whitespace", function () {
const certificate = keyInfoToPem("QUIK \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQUIK \n-----END CERTIFICATE-----\n",
);
});

it("should allow one Base64 pad with trailing whitespace", function () {
const certificate = keyInfoToPem("QQo= \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQQo= \n-----END CERTIFICATE-----\n",
);
});

it("should allow two Base64 pads with trailing whitespace", function () {
const certificate = keyInfoToPem("QUJDCg== \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQUJDCg== \n-----END CERTIFICATE-----\n",
);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Test expectations don't match the implementation behavior.

These three tests expect trailing spaces to be preserved in the PEM output, but the implementation in src/crypto.ts line 63 calls .trim() on the input, which removes all trailing whitespace (both spaces and newlines).

For example, the first test provides input "QUIK \n" and expects the output to contain "QUIK " (with space) in the PEM body. However, after bufferToString(keyInfo)?.trim() is called, the result will be "QUIK" (no space), so the actual output will be:

-----BEGIN CERTIFICATE-----\nQUIK\n-----END CERTIFICATE-----\n

not:

-----BEGIN CERTIFICATE-----\nQUIK \n-----END CERTIFICATE-----\n

The same issue affects all three tests (lines 153-172). These tests will fail when executed.

To fix this, you need to decide on the correct behavior:

Option 1: If trailing whitespace should be removed (current implementation), update test expectations:

-      it("should allow Base64 without padding but with trailing whitespace", function () {
+      it("should accept Base64 without padding with trailing whitespace (whitespace trimmed)", function () {
         const certificate = keyInfoToPem("QUIK \n", "CERTIFICATE");
         expect(certificate).to.equal(
-          "-----BEGIN CERTIFICATE-----\nQUIK \n-----END CERTIFICATE-----\n",
+          "-----BEGIN CERTIFICATE-----\nQUIK\n-----END CERTIFICATE-----\n",
         );
       });
 
-      it("should allow one Base64 pad with trailing whitespace", function () {
+      it("should accept one Base64 pad with trailing whitespace (whitespace trimmed)", function () {
         const certificate = keyInfoToPem("QQo= \n", "CERTIFICATE");
         expect(certificate).to.equal(
-          "-----BEGIN CERTIFICATE-----\nQQo= \n-----END CERTIFICATE-----\n",
+          "-----BEGIN CERTIFICATE-----\nQQo=\n-----END CERTIFICATE-----\n",
         );
       });
 
-      it("should allow two Base64 pads with trailing whitespace", function () {
+      it("should accept two Base64 pads with trailing whitespace (whitespace trimmed)", function () {
         const certificate = keyInfoToPem("QUJDCg== \n", "CERTIFICATE");
         expect(certificate).to.equal(
-          "-----BEGIN CERTIFICATE-----\nQUJDCg== \n-----END CERTIFICATE-----\n",
+          "-----BEGIN CERTIFICATE-----\nQUJDCg==\n-----END CERTIFICATE-----\n",
         );
       });

Option 2: If trailing whitespace should be preserved (per the test expectations and PR description "does not trim whitespace for generated PEM output"), modify src/crypto.ts line 63 to only trim for validation but preserve the original data for output. However, this would conflict with normalizePemFile which is designed to produce normalized output per RFC 7468.

Based on RFC 7468 and the fact that normalizePemFile is explicitly designed to produce normalized output, Option 1 (trimming whitespace) appears to be the correct approach.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should allow Base64 without padding but with trailing whitespace", function () {
const certificate = keyInfoToPem("QUIK \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQUIK \n-----END CERTIFICATE-----\n",
);
});
it("should allow one Base64 pad with trailing whitespace", function () {
const certificate = keyInfoToPem("QQo= \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQQo= \n-----END CERTIFICATE-----\n",
);
});
it("should allow two Base64 pads with trailing whitespace", function () {
const certificate = keyInfoToPem("QUJDCg== \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQUJDCg== \n-----END CERTIFICATE-----\n",
);
});
it("should accept Base64 without padding with trailing whitespace (whitespace trimmed)", function () {
const certificate = keyInfoToPem("QUIK \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQUIK\n-----END CERTIFICATE-----\n",
);
});
it("should accept one Base64 pad with trailing whitespace (whitespace trimmed)", function () {
const certificate = keyInfoToPem("QQo= \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQQo=\n-----END CERTIFICATE-----\n",
);
});
it("should accept two Base64 pads with trailing whitespace (whitespace trimmed)", function () {
const certificate = keyInfoToPem("QUJDCg== \n", "CERTIFICATE");
expect(certificate).to.equal(
"-----BEGIN CERTIFICATE-----\nQUJDCg==\n-----END CERTIFICATE-----\n",
);
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

X Tutup