Skip to content

fix(namecheap): lowercase HostName for setHosts API compat#39

Merged
albedosehen merged 1 commit into
mainfrom
fix/namecheap-lowercase-hostname
May 9, 2026
Merged

fix(namecheap): lowercase HostName for setHosts API compat#39
albedosehen merged 1 commit into
mainfrom
fix/namecheap-lowercase-hostname

Conversation

@albedosehen
Copy link
Copy Markdown
Contributor

What

Namecheap's domains.dns.setHosts validates HostName case-sensitively and rejects uppercase letters with 2050900: INVALID_NAME. DNS itself is case-insensitive at resolution time, but Namecheap's input validator is not.

Sectigo's CSR-hash CNAME response delivers the host portion as uppercase MD5 hex (e.g. _442EBF5636AF...). Without normalization, every Sectigo CSR-hash renewal via PR #38 dies at the publish step.

Verified directly

Manual setHosts call against Namecheap, same payload, only difference being host case:

  • Uppercase hostname: 2050900: INVALID_NAME
  • Lowercase hostname: accepted

Existing accepted CNAME already on oneiric.dev (from prior provisioning): _3150e77263e8e7664a9f0d59993747de — lowercase. Pattern confirmed.

Fix

split_record_name lowercases subdomain, sld, and tld via to_ascii_lowercase. New unit test pins the exact Sectigo shape.

Verified

  • cargo fmt --all --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace --locked 153 tests pass (108 daemon, +1 new)

Namecheap's domains.dns.setHosts API validates HostName values
case-sensitively and rejects uppercase letters with:

  2050900: INVALID_NAME: Host name: '_<UPPERCASE_HEX>' is invalid.

DNS itself is case-insensitive at resolution time, but Namecheap's
input validator is not. Sectigo's CSR-hash CNAME response delivers
the host portion as uppercase MD5 hex (e.g. `_442EBF5636AF...`).
Without normalization, every Sectigo CSR-hash renewal blows up at
the publish step.

Confirmed against Namecheap's API directly with a manual setHosts
call: identical request body with the uppercase hostname returns
2050900; lowercasing the host portion is the only difference and
the call succeeds. The accepted CNAME already on oneiric.dev (from
a prior provisioning) uses lowercase hex, matching this behavior.

Fix: split_record_name normalizes subdomain + sld + tld to ASCII
lowercase. SLD/TLD were already-lowercase by convention but cheap
to defend against; the real workhorse is the subdomain lowercase.
Sectigo's validator does a case-insensitive DNS lookup so the
record still resolves correctly.

Test pinning the case normalization against the exact Sectigo
CSR-hash hostname shape (`_<UPPERCASE_HEX>.<MixedCase.Tld>`).
@albedosehen albedosehen merged commit ad256f6 into main May 9, 2026
1 check passed
@albedosehen albedosehen deleted the fix/namecheap-lowercase-hostname branch May 9, 2026 18:34
albedosehen added a commit that referenced this pull request May 9, 2026
rota's `get_info` was looking for `<CertificateReturned>` element
text and `<CACertificate>` element text. Neither matches Namecheap's
actual `ssl.getInfo&Returncertificate=true` response, which carries
`CertificateReturned` as an ATTRIBUTE on `<Certificates>` and packs
PEMs in nested `<Certificate>` elements:

  <Certificates CertificateReturned="true" ReturnType="INDIVIDUAL">
    <Certificate><![CDATA[LEAF_PEM]]></Certificate>
    <CaCertificates>
      <Certificate Type="INTERMEDIATE">
        <Certificate><![CDATA[INTERMEDIATE_1_PEM]]></Certificate>
      </Certificate>
      ...
    </CaCertificates>
  </Certificates>

Result: cert_pem and chain_pem both empty, `is_issued()` false,
polling never terminates even when status==active. So PR #40's
chain-follow lands on the right SSL ID but `await_issuance` still
hangs at the extraction step. Found by extracting the cert manually
out of band when `getInfo` returned status=active for oneiric.dev's
in-flight order: rota's parser yielded empty strings even though
the PEMs were sitting right there in the response.

Fix: new `ApiResponse::pem_blocks(label)` method scans the raw
response for `-----BEGIN <label>-----`...`-----END <label>-----`
armor and returns each block in document order. `get_info` calls
`pem_blocks("CERTIFICATE")`; first block is the leaf, rest are the
chain (concatenated with newlines). The CSR present in the same
response is safely skipped because its label is "CERTIFICATE
REQUEST" and `BEGIN CERTIFICATE-----` doesn't substring-match
`BEGIN CERTIFICATE REQUEST-----`.

This is the 6th and (hopefully) final layer in the rota+Namecheap
end-to-end renewal pipeline, after PRs #36 (reverted), #37 (CDATA
unwrap), #38 (DnsCname variant), #39 (lowercase HostName), #40
(ReplacedBy chain + Status XML path). Tests: 3 new in xml::tests
covering the leaf+chain extraction, the CSR-skip rule, and the
empty-input edge case. Total daemon test count 111 (was 108).
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