From 378f7a1830478f4280563b5d68c371dd083e0d2f Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Tue, 12 May 2026 12:05:58 -0500 Subject: [PATCH] fix(lotw): normalize CRLF to LF in tCERT body so LoTW accepts uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit node-forge emits PEM with \r\n line endings (pem.js:46,82,84 and util.js:1602 encode64 wraps at maxline=64 with \r\n). Wavelog's reference .tq8 — which we're matching — comes from PHP's openssl_x509_export, which uses \n. So our tCERT block ships the certificate body with CRLF between base64 lines and LoTW's processor rejects the QSOs silently. The symptom is exactly what K1AF saw: LoTW's upload endpoint replies " File queued for processing" (multipart POST received, file in queue), but the queue worker then drops the file in a way that doesn't move the operator's "Last upload" timestamp on the Logbook Call Sign Activity page. Rejection email goes to the cert holder. Fix: collapse \r\n (and any stray \r) to \n in certPemBody after stripping the BEGIN/END markers. This does NOT affect the per-QSO RSA-SHA1 signatures — those are over the canonical sign-string, not over the PEM. Only the cert body emitted in the tCERT record needs to match the reference line-ending convention. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/lotw.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib/lotw.ts b/src/lib/lotw.ts index a6b08bf..df595dc 100644 --- a/src/lib/lotw.ts +++ b/src/lib/lotw.ts @@ -300,11 +300,19 @@ function parseP12(buf: Buffer, password: string): ParsedP12 { const privateKeyPem = forge.pki.privateKeyToPem(keyBag.key); const certPem = forge.pki.certificateToPem(certBag.cert); - // Strip exactly the BEGIN/END markers; keep internal newlines unchanged - // (LoTW's parser expects multi-line PEM bodies). + // Strip exactly the BEGIN/END markers, keep internal newlines, and normalize + // CRLF to LF. node-forge emits PEM with \r\n (pem.js, util.js encode64), but + // wavelog's reference .tq8 is produced from PHP's openssl_x509_export which + // uses \n only. LoTW silently rejects the QSOs in a file whose tCERT body + // doesn't match TQSL's expected line-ending convention (the file is still + // queued for processing — the "" marker just means + // the multipart POST was received — but the processor drops everything + // before the "Last upload" timestamp moves). const certPemBody = certPem .replace(/-----BEGIN CERTIFICATE-----\s*/g, '') .replace(/-----END CERTIFICATE-----\s*/g, '') + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') .trim(); // ARRL embeds DXCC entity ID in a private extension (PrintableString).