Skip to content

fix(lotw): normalize CRLF to LF in tCERT body so LoTW accepts uploads#213

Merged
patrickrb merged 1 commit into
mainfrom
fix/lotw-cert-pem-line-endings
May 12, 2026
Merged

fix(lotw): normalize CRLF to LF in tCERT body so LoTW accepts uploads#213
patrickrb merged 1 commit into
mainfrom
fix/lotw-cert-pem-line-endings

Conversation

@patrickrb
Copy link
Copy Markdown
Owner

@patrickrb patrickrb commented May 12, 2026

Summary

Almost certainly the reason K1AF's LoTW "Last upload" timestamp hasn't moved from 2025-07-28 despite every upload from this app reporting success since #185.

What's happening:

  • node-forge's certificateToPem produces PEM with \r\n line endings (node_modules/node-forge/lib/pem.js:46,82,84 and util.js:1602encode64 wraps base64 at maxline=64 with \r\n).
  • The wavelog reference .tq8 we're matching comes from PHP's openssl_x509_export, which uses \n only.
  • So our tCERT block ships the certificate body with CRLF between base64 lines instead of LF. LoTW's queue processor silently rejects every QSO in the file.

Why the upload looks like it succeeded:

LoTW's upload endpoint replies <!-- .UPL. accepted --> File queued for processing as soon as the multipart POST is received — that marker means "your file is in our queue", not "we processed it". The queue worker runs later, parses the .tq8, and (in this case) drops everything because the cert body doesn't match the expected line-ending convention. Operator "Last upload" timestamp on Logbook Call Sign Activity doesn't move, and the cert holder gets a rejection email.

Fix

Collapse \r\n (and any stray \r) to \n in certPemBody after stripping the BEGIN/END markers, before emitting in tCERT.

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 the LoTW parser ingests needs to match.

Test plan

After deploying:

  • Run the SQL reset from fix(sync): emit MHz frequency to LoTW + QRZ instead of dividing by 1M #212 again (or just for new QSOs since last sync) to clear lotw_qsl_sent='Y'
  • Trigger upload on /lotw → "Upload now"
  • Wait 10-30 min for LoTW's queue, then check Logbook Call Sign Activity for K1AF — "Last upload" should now show today's UTC time, not 2025-07-28
  • Open Your QSOs on LoTW → the new QSOs should appear
  • Verify the cert-holder email got the LoTW processing confirmation instead of the rejection notice

🤖 Generated with Claude Code

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
"<!-- .UPL. accepted --> 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) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodelog Ready Ready Preview, Comment May 12, 2026 5:06pm

Request Review

@patrickrb patrickrb merged commit 8e9d115 into main May 12, 2026
7 checks passed
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