Accept Base64 format with trailing whitespace#409
Accept Base64 format with trailing whitespace#409mikkosuonio wants to merge 3 commits intonode-saml:masterfrom
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 ifkeyInfois a non‑string/non‑Buffer at runtime (e.g., the test that passesfalse) becausevalue?.trim()still attempts to call an undefinedtrimfunction on booleans. MakebufferToStringdefensively 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 PEMInterpolating 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 inputsArrow 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
📒 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 whitespaceThis validates the intended relaxation and preserves whitespace inside the PEM body.
158-161: LGTM: accepts single '=' padding + trailing whitespaceCovers a common encoder output variant.
163-166: LGTM: accepts '==' padding + trailing whitespaceGood coverage of padding cases with whitespace.
This was initially proposed as a fix to node-saml#361
This should ensure 'latin1' encoding is used for keyInfo of type Buffer.
fedf665 to
1b39a96
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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
bufferToStringhelper useslatin1encoding when converting Buffers. While this works correctly for Base64/PEM data (which uses ASCII characters), the defaulttoString()encoding isutf8.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:
- Using
utf8(default) for consistency with common Node.js patterns- Adding a comment explaining why
latin1was chosen for this use caseIf
latin1was 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(); |
There was a problem hiding this comment.
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:
- PR description: States "The fix does not trim whitespace for generated PEM output" — but the implementation DOES trim (line 63) and normalizes the output.
- Test expectations in
test/crypto.spec.ts(lines 153-172): Expect trailing spaces to be preserved in the output, buttrim()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:
- The PR description to clarify that trailing whitespace is trimmed from output (not preserved)
- 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.
| 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", | ||
| ); | ||
| }); |
There was a problem hiding this comment.
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.
| 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", | |
| ); | |
| }); |
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
Tests